Przeglądaj źródła

fix(stats): lazy-init web stats and degrade non-PSRAM boards to live-only

Jared Dohrman 3 miesięcy temu
rodzic
commit
b2ea2f882d

+ 54 - 36
examples/simple_repeater/MyMesh.cpp

@@ -1077,7 +1077,7 @@ void MyMesh::begin(FILESYSTEM *fs, ArchiveStorage* archive) {
   web.setNetworkStateProvider(&network);
   web.begin(_fs);
   _stats_history.begin(web.isWebStatsEnabled(), _archive);
-  if (web.isWebStatsEnabled() && _archive != nullptr && _archive->isMounted()) {
+  if (web.isWebStatsEnabled() && !_stats_history.isLiveOnly() && _archive != nullptr && _archive->isMounted()) {
     restoreArchiveNeighbours();
     next_archive_neighbours_flush_ms = millis() + kArchiveNeighboursFlushIntervalMs;
   }
@@ -1449,6 +1449,7 @@ void MyMesh::updateStatsHistory(unsigned long now_ms) {
     _archive_neighbours_dirty = false;
     return;
   }
+  _stats_history.maybeReleaseIdleBuffers(now_ms);
 
   const bool wifi_connected = network.isWifiConnected();
 #ifdef WITH_MQTT_UPLINK
@@ -1460,6 +1461,7 @@ void MyMesh::updateStatsHistory(unsigned long now_ms) {
   const bool archive_mounted = (_archive != nullptr) && _archive->isMounted();
 #if defined(ESP32)
   const uint32_t free_heap = ESP.getFreeHeap();
+  const uint32_t max_alloc_heap = ESP.getMaxAllocHeap();
   const uint32_t uptime_secs = static_cast<uint32_t>(uptime_millis / 1000);
   bool low_memory = _stats_state.low_memory;
   if (!_stats_state.initialized) {
@@ -1493,14 +1495,14 @@ void MyMesh::updateStatsHistory(unsigned long now_ms) {
     if (_stats_state.archive_mounted != archive_mounted) {
       recordStatsEvent(archive_mounted ? HISTORY_EVENT_ARCHIVE_MOUNTED : HISTORY_EVENT_ARCHIVE_UNAVAILABLE);
       _stats_state.archive_mounted = archive_mounted;
-      if (archive_mounted) {
+      if (!_stats_history.isLiveOnly() && archive_mounted) {
         if (getNeighbourCount() == 0) {
           restoreArchiveNeighbours();
         }
         next_archive_neighbours_flush_ms = now_ms + kArchiveNeighboursFlushIntervalMs;
       }
     }
-    if (!_stats_state.low_memory && low_memory) {
+    if (!_stats_history.isLiveOnly() && !_stats_state.low_memory && low_memory) {
 #if defined(ESP32)
       if (_stats_state.last_low_memory_event_uptime_secs == 0 ||
           (uptime_secs - _stats_state.last_low_memory_event_uptime_secs) >= kLowMemoryEventCooldownSecs) {
@@ -1513,43 +1515,55 @@ void MyMesh::updateStatsHistory(unsigned long now_ms) {
     _stats_state.low_memory = low_memory;
   }
 
+#if defined(ESP32)
+  const bool live_stats_headroom_low =
+      _stats_history.isLiveOnly() && (free_heap <= kLowMemoryClearBytes || max_alloc_heap <= (24UL * 1024UL));
+#else
+  const bool live_stats_headroom_low = false;
+#endif
   if (next_history_sample_ms == 0 || millisHasNowPassed(next_history_sample_ms)) {
-    HistorySample sample{};
-    sample.epoch_secs = getRTCClock()->getCurrentTime();
-    sample.uptime_secs = static_cast<uint32_t>(uptime_millis / 1000);
-    sample.packets_sent = radio_driver.getPacketsSent();
-    sample.packets_recv = radio_driver.getPacketsRecv();
-    sample.battery_mv = board.getBattMilliVolts();
-    sample.queue_len = static_cast<uint16_t>(_mgr->getOutboundTotal());
-    sample.error_flags = _err_flags;
-    sample.recv_errors = static_cast<uint16_t>(min<uint32_t>(radio_driver.getPacketsRecvErrors(), 0xFFFF));
-    sample.neighbour_count = static_cast<uint16_t>(min<size_t>(getNeighbourCount(), 0xFFFF));
-    sample.direct_dups = static_cast<uint16_t>(min<uint32_t>(((SimpleMeshTables *)getTables())->getNumDirectDups(), 0xFFFF));
-    sample.flood_dups = static_cast<uint16_t>(min<uint32_t>(((SimpleMeshTables *)getTables())->getNumFloodDups(), 0xFFFF));
-    sample.last_rssi_x4 = static_cast<int16_t>(radio_driver.getLastRSSI() * 4.0f);
-    sample.last_snr_x4 = static_cast<int16_t>(radio_driver.getLastSNR() * 4.0f);
-    sample.noise_floor = static_cast<int16_t>(_radio->getNoiseFloor());
-    sample.battery_pct = static_cast<int8_t>(board.getBatteryPercent());
+    if (!live_stats_headroom_low) {
+      HistorySample sample{};
+      sample.epoch_secs = getRTCClock()->getCurrentTime();
+      sample.uptime_secs = static_cast<uint32_t>(uptime_millis / 1000);
+      sample.packets_sent = radio_driver.getPacketsSent();
+      sample.packets_recv = radio_driver.getPacketsRecv();
+      sample.battery_mv = board.getBattMilliVolts();
+      sample.queue_len = static_cast<uint16_t>(_mgr->getOutboundTotal());
+      sample.error_flags = _err_flags;
+      sample.recv_errors = static_cast<uint16_t>(min<uint32_t>(radio_driver.getPacketsRecvErrors(), 0xFFFF));
+      sample.neighbour_count = static_cast<uint16_t>(min<size_t>(getNeighbourCount(), 0xFFFF));
+      sample.direct_dups =
+          static_cast<uint16_t>(min<uint32_t>(((SimpleMeshTables *)getTables())->getNumDirectDups(), 0xFFFF));
+      sample.flood_dups =
+          static_cast<uint16_t>(min<uint32_t>(((SimpleMeshTables *)getTables())->getNumFloodDups(), 0xFFFF));
+      sample.last_rssi_x4 = static_cast<int16_t>(radio_driver.getLastRSSI() * 4.0f);
+      sample.last_snr_x4 = static_cast<int16_t>(radio_driver.getLastSNR() * 4.0f);
+      sample.noise_floor = static_cast<int16_t>(_radio->getNoiseFloor());
+      sample.battery_pct = static_cast<int8_t>(board.getBatteryPercent());
 #if defined(ESP32)
-    sample.heap_free = ESP.getFreeHeap();
-    sample.heap_min = ESP.getMinFreeHeap();
-    sample.psram_free = ESP.getFreePsram();
-    sample.psram_min = ESP.getMinFreePsram();
-#endif
-    if (board.isExternalPowered()) sample.flags |= HISTORY_FLAG_EXTERNAL_POWER;
-    if (board.isCharging()) sample.flags |= HISTORY_FLAG_CHARGING;
-    if (board.isVbusPresent()) sample.flags |= HISTORY_FLAG_VBUS;
-    if (wifi_connected) sample.flags |= HISTORY_FLAG_WIFI_CONNECTED;
-    if (mqtt_connected) sample.flags |= HISTORY_FLAG_MQTT_CONNECTED;
-    if (web.isWebEnabled()) sample.flags |= HISTORY_FLAG_WEB_ENABLED;
-    if (web_panel_up) sample.flags |= HISTORY_FLAG_WEB_PANEL_UP;
-    if (archive_mounted) sample.flags |= HISTORY_FLAG_ARCHIVE_MOUNTED;
-    _stats_history.pushSample(sample);
+      sample.heap_free = free_heap;
+      sample.heap_min = ESP.getMinFreeHeap();
+      sample.psram_free = ESP.getFreePsram();
+      sample.psram_min = ESP.getMinFreePsram();
+#endif
+      if (board.isExternalPowered()) sample.flags |= HISTORY_FLAG_EXTERNAL_POWER;
+      if (board.isCharging()) sample.flags |= HISTORY_FLAG_CHARGING;
+      if (board.isVbusPresent()) sample.flags |= HISTORY_FLAG_VBUS;
+      if (wifi_connected) sample.flags |= HISTORY_FLAG_WIFI_CONNECTED;
+      if (mqtt_connected) sample.flags |= HISTORY_FLAG_MQTT_CONNECTED;
+      if (web.isWebEnabled()) sample.flags |= HISTORY_FLAG_WEB_ENABLED;
+      if (web_panel_up) sample.flags |= HISTORY_FLAG_WEB_PANEL_UP;
+      if (archive_mounted) sample.flags |= HISTORY_FLAG_ARCHIVE_MOUNTED;
+      _stats_history.pushSample(sample);
+    }
     next_history_sample_ms = now_ms + 60000UL;
   }
 
   _stats_history.maybeFlush(now_ms);
-  maybeFlushArchiveNeighbours(now_ms);
+  if (!_stats_history.isLiveOnly()) {
+    maybeFlushArchiveNeighbours(now_ms);
+  }
 #else
   (void)now_ms;
 #endif
@@ -1845,11 +1859,12 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply
     web.formatWebStatusReply(reply, 160);
   } else if (strcmp(command, "get web.stats.status") == 0) {
     snprintf(reply, 160,
-             "> enabled:%s history:%s psram:%s degraded:%s samples:%u/%u events:%u/%u archive:%s logical:%s path:%s",
+             "> enabled:%s history:%s psram:%s degraded:%s mode:%s samples:%u/%u events:%u/%u archive:%s logical:%s path:%s",
              web.isWebStatsEnabled() ? "on" : "off",
              (_stats_history.isEnabled() && _stats_history.isRecentHistoryAvailable()) ? "active" : "inactive",
              _stats_history.isPsramBacked() ? "yes" : "no",
              _stats_history.isDegraded() ? "yes" : "no",
+             _stats_history.isLiveOnly() ? "live" : "full",
              static_cast<unsigned>(_stats_history.getSampleCount()),
              static_cast<unsigned>(_stats_history.getSampleCapacity()),
              static_cast<unsigned>(_stats_history.getEventCount()),
@@ -2235,11 +2250,12 @@ bool MyMesh::formatWebStatsSummaryJson(char* reply, size_t reply_size) {
   const char* archive_name = (_archive != nullptr) ? _archive->getLogicalName() : "archive";
   const char* archive_path = (_archive != nullptr) ? _archive->getLogicalStatsPath() : "archive:/stats";
   const char* archive_type = (_archive != nullptr) ? _archive->getCardTypeName() : "unavailable";
+  _stats_history.noteAccess(millis());
 
   size_t offset = 0;
   offset += snprintf(&reply[offset], reply_size - offset,
                      "{\"enabled\":true,"
-                     "\"history\":{\"active\":%s,\"psram\":%s,\"degraded\":%s,\"samples\":%u,\"sample_capacity\":%u,\"sample_interval_secs\":%lu,"
+                     "\"history\":{\"active\":%s,\"psram\":%s,\"degraded\":%s,\"live_only\":%s,\"samples\":%u,\"sample_capacity\":%u,\"sample_interval_secs\":%lu,"
                      "\"archive_restored\":%s,\"archive_restored_samples\":%u,\"archive_summary_interval_secs\":%lu,"
                      "\"events\":%u,\"event_capacity\":%u},"
                      "\"archive\":{\"logical\":\"%s\",\"available\":%s,\"path\":\"%s\",\"type\":\"%s\","
@@ -2256,6 +2272,7 @@ bool MyMesh::formatWebStatsSummaryJson(char* reply, size_t reply_size) {
                      (_stats_history.isEnabled() && _stats_history.isRecentHistoryAvailable()) ? "true" : "false",
                      _stats_history.isPsramBacked() ? "true" : "false",
                      _stats_history.isDegraded() ? "true" : "false",
+                     _stats_history.isLiveOnly() ? "true" : "false",
                      static_cast<unsigned>(_stats_history.getSampleCount()),
                      static_cast<unsigned>(_stats_history.getSampleCapacity()),
                      static_cast<unsigned long>(StatsHistory::getSampleIntervalSecs()),
@@ -2339,6 +2356,7 @@ bool MyMesh::formatWebStatsSeriesJson(const char* series, char* reply, size_t re
     }
     return false;
   }
+  _stats_history.noteAccess(millis());
   return _stats_history.buildSeriesJson(
       series,
       reply,

+ 114 - 33
src/helpers/StatsHistory.cpp

@@ -28,6 +28,8 @@ constexpr uint32_t kEventFlushIntervalMs = 60UL * 1000UL;
 constexpr size_t kMaxSeriesPoints = 64;
 constexpr size_t kSummaryRestoreWindowBytes = 16384;
 constexpr size_t kEventsRestoreWindowBytes = 4096;
+constexpr size_t kLiveOnlySampleCapacity = 24;
+constexpr size_t kLiveOnlyEventCapacity = 8;
 
 struct HistoryCapacityBucket {
   size_t sample_capacity;
@@ -37,7 +39,7 @@ struct HistoryCapacityBucket {
 HistoryCapacityBucket getHistoryCapacityBucket(bool want_psram) {
 #if defined(ESP32)
   if (!want_psram) {
-    return {96, 32};
+    return {kLiveOnlySampleCapacity, kLiveOnlyEventCapacity};
   }
 
   const uint32_t psram_size = ESP.getPsramSize();
@@ -54,6 +56,14 @@ HistoryCapacityBucket getHistoryCapacityBucket(bool want_psram) {
 #endif
 }
 
+bool shouldUseLiveOnlyStats() {
+#if defined(ESP32)
+  return !psramFound();
+#else
+  return false;
+#endif
+}
+
 template <typename T>
 T* allocHistoryBuffer(size_t count) {
 #if defined(ESP32)
@@ -251,10 +261,10 @@ uint8_t eventTypeFromName(const char* name) {
 }  // namespace
 
 StatsHistory::StatsHistory()
-    : _enabled(false), _psram_backed(false), _degraded(false), _summary_dirty(false), _next_summary_flush_ms(0),
-      _next_event_flush_ms(0), _sample_capacity(0), _sample_head(0), _sample_count(0), _event_capacity(0),
-      _event_head(0), _event_count(0), _samples(nullptr), _events(nullptr), _archive(nullptr), _restored_sample_count(0),
-      _pending_event_count(0) {
+    : _enabled(false), _activated(false), _psram_backed(false), _degraded(false), _live_only(false),
+      _summary_dirty(false), _next_summary_flush_ms(0), _next_event_flush_ms(0), _last_access_ms(0),
+      _sample_capacity(0), _sample_head(0), _sample_count(0), _event_capacity(0), _event_head(0), _event_count(0),
+      _samples(nullptr), _events(nullptr), _archive(nullptr), _restored_sample_count(0), _pending_event_count(0) {
 }
 
 StatsHistory::~StatsHistory() {
@@ -264,11 +274,12 @@ StatsHistory::~StatsHistory() {
 
 void StatsHistory::begin(bool enabled, ArchiveStorage* archive) {
   _archive = archive;
+  _live_only = shouldUseLiveOnlyStats();
+  _degraded = _live_only;
+  _psram_backed = !_live_only;
+  _activated = false;
+  _last_access_ms = 0;
   _enabled = enabled;
-  ensureBuffers();
-  if (_enabled && _sample_count == 0 && isArchiveAvailable()) {
-    restoreSummaryLog();
-  }
   _next_summary_flush_ms = millis() + kSummaryFlushIntervalMs;
   _next_event_flush_ms = millis() + kEventFlushIntervalMs;
 }
@@ -278,13 +289,23 @@ void StatsHistory::setArchive(ArchiveStorage* archive) {
 }
 
 void StatsHistory::setEnabled(bool enabled) {
-  if (enabled && !ensureBuffers()) {
+  const bool was_enabled = _enabled;
+  _live_only = shouldUseLiveOnlyStats();
+  _degraded = _live_only;
+  _psram_backed = !_live_only;
+
+  if (!enabled) {
     _enabled = false;
+    releaseBuffers();
     return;
   }
-  _enabled = enabled;
-  if (_enabled && _sample_count == 0 && isArchiveAvailable()) {
-    restoreSummaryLog();
+
+  _enabled = true;
+  if (!was_enabled) {
+    _activated = false;
+    _last_access_ms = 0;
+    _next_summary_flush_ms = millis() + kSummaryFlushIntervalMs;
+    _next_event_flush_ms = millis() + kEventFlushIntervalMs;
   }
 }
 
@@ -292,26 +313,42 @@ bool StatsHistory::isArchiveAvailable() const {
   return _archive != nullptr && _archive->isMounted();
 }
 
+bool StatsHistory::activate() {
+  if (_activated) {
+    return true;
+  }
+  if (!ensureBuffers()) {
+    return false;
+  }
+  _activated = true;
+  if (supportsPersistence() && isArchiveAvailable()) {
+    if (_sample_count == 0) {
+      restoreSummaryLog();
+    }
+    if (_event_count == 0) {
+      restoreEventsLog();
+    }
+  }
+  return true;
+}
+
 bool StatsHistory::ensureBuffers() {
-  if (_samples != nullptr && _events != nullptr) {
+  if (_samples != nullptr && (_event_capacity == 0 || _events != nullptr)) {
     return true;
   }
 
-#if defined(ESP32)
-  const bool want_psram = psramFound();
-#else
-  const bool want_psram = false;
-#endif
+  const bool want_psram = !shouldUseLiveOnlyStats();
+  _live_only = !want_psram;
+  _degraded = _live_only;
+  _psram_backed = want_psram;
 
   const HistoryCapacityBucket bucket = getHistoryCapacityBucket(want_psram);
   _sample_capacity = bucket.sample_capacity;
   _event_capacity = bucket.event_capacity;
   _samples = allocHistoryBuffer<HistorySample>(_sample_capacity);
-  _events = allocHistoryBuffer<HistoryEvent>(_event_capacity);
-  _psram_backed = want_psram;
-  _degraded = !want_psram;
+  _events = (_event_capacity > 0) ? allocHistoryBuffer<HistoryEvent>(_event_capacity) : nullptr;
 
-  if (_samples == nullptr || _events == nullptr) {
+  if (_samples == nullptr || (_event_capacity > 0 && _events == nullptr)) {
     freeHistoryBuffer(_samples);
     freeHistoryBuffer(_events);
     _samples = allocHistoryBuffer<HistorySample>(64);
@@ -320,9 +357,53 @@ bool StatsHistory::ensureBuffers() {
     _event_capacity = (_events != nullptr) ? 24 : 0;
     _psram_backed = false;
     _degraded = true;
+    _live_only = true;
   }
 
-  return _samples != nullptr && _events != nullptr;
+  return _samples != nullptr && (_event_capacity == 0 || _events != nullptr);
+}
+
+void StatsHistory::releaseBuffers() {
+  freeHistoryBuffer(_samples);
+  freeHistoryBuffer(_events);
+  _samples = nullptr;
+  _events = nullptr;
+  _activated = false;
+  _last_access_ms = 0;
+  _sample_capacity = 0;
+  _sample_head = 0;
+  _sample_count = 0;
+  _event_capacity = 0;
+  _event_head = 0;
+  _event_count = 0;
+  _summary_dirty = false;
+  _pending_event_count = 0;
+  _restored_sample_count = 0;
+}
+
+bool StatsHistory::supportsPersistence() const {
+  return !_live_only;
+}
+
+bool StatsHistory::isAccessActive(uint32_t now_ms) const {
+  if (!_live_only) {
+    return true;
+  }
+  return _last_access_ms != 0 && (now_ms - _last_access_ms) < kLiveOnlyIdleTimeoutMs;
+}
+
+void StatsHistory::noteAccess(uint32_t now_ms) {
+  _last_access_ms = now_ms;
+  if (_enabled) {
+    activate();
+  }
+}
+
+void StatsHistory::maybeReleaseIdleBuffers(uint32_t now_ms) {
+  if (!_enabled || !_live_only || !isRecentHistoryAvailable() || isAccessActive(now_ms)) {
+    return;
+  }
+  releaseBuffers();
 }
 
 void StatsHistory::storeSample(const HistorySample& sample, bool mark_dirty) {
@@ -408,7 +489,7 @@ bool StatsHistory::parseSummaryLine(const char* line, HistorySample& sample) con
 }
 
 bool StatsHistory::restoreSummaryLog() {
-  if (!isArchiveAvailable() || _sample_capacity == 0) {
+  if (!supportsPersistence() || !isArchiveAvailable() || _sample_capacity == 0) {
     return false;
   }
 
@@ -507,7 +588,7 @@ bool StatsHistory::parseEventLine(const char* line, HistoryEvent& event) const {
 }
 
 bool StatsHistory::restoreEventsLog() {
-  if (!isArchiveAvailable() || _event_capacity == 0) {
+  if (!supportsPersistence() || !isArchiveAvailable() || _event_capacity == 0) {
     return false;
   }
 
@@ -573,7 +654,7 @@ bool StatsHistory::restoreEventsLog() {
 }
 
 void StatsHistory::pushSample(const HistorySample& sample) {
-  if (!_enabled || !ensureBuffers()) {
+  if (!_enabled || !_activated || (_live_only && !isAccessActive(millis())) || !ensureBuffers()) {
     return;
   }
   storeSample(sample, true);
@@ -590,7 +671,7 @@ void StatsHistory::appendPendingEvent(const HistoryEvent& event) {
 }
 
 void StatsHistory::storeEvent(const HistoryEvent& event, bool queue_pending) {
-  if (!ensureBuffers()) {
+  if (_event_capacity == 0 || !ensureBuffers()) {
     return;
   }
 
@@ -606,7 +687,7 @@ void StatsHistory::storeEvent(const HistoryEvent& event, bool queue_pending) {
 }
 
 void StatsHistory::recordEvent(uint8_t type, uint32_t epoch_secs, uint32_t uptime_secs, int16_t value) {
-  if (!ensureBuffers()) {
+  if (!_enabled || !_activated || (_live_only && !isAccessActive(millis())) || _event_capacity == 0 || !ensureBuffers()) {
     return;
   }
 
@@ -619,7 +700,7 @@ void StatsHistory::recordEvent(uint8_t type, uint32_t epoch_secs, uint32_t uptim
 }
 
 void StatsHistory::maybeFlush(uint32_t now_ms) {
-  if (!_enabled || !isArchiveAvailable()) {
+  if (!_enabled || !_activated || !supportsPersistence() || !isArchiveAvailable()) {
     return;
   }
 
@@ -636,7 +717,7 @@ void StatsHistory::maybeFlush(uint32_t now_ms) {
 }
 
 void StatsHistory::flushSummaryLog() {
-  if (_archive == nullptr || _sample_count == 0) {
+  if (!supportsPersistence() || _archive == nullptr || _sample_count == 0) {
     return;
   }
 
@@ -686,7 +767,7 @@ void StatsHistory::flushSummaryLog() {
 }
 
 void StatsHistory::flushEventsLog() {
-  if (_archive == nullptr || _pending_event_count == 0) {
+  if (!supportsPersistence() || _archive == nullptr || _pending_event_count == 0) {
     return;
   }
 
@@ -724,7 +805,7 @@ void StatsHistory::flushEventsLog() {
 }
 
 void StatsHistory::writeMetaFile() const {
-  if (_archive == nullptr) {
+  if (!supportsPersistence() || _archive == nullptr) {
     return;
   }
   FILESYSTEM* fs = _archive->getFS();

+ 11 - 0
src/helpers/StatsHistory.h

@@ -65,6 +65,7 @@ class StatsHistory {
 public:
   static constexpr uint32_t kSampleIntervalSecs = 60;
   static constexpr uint32_t kArchiveSummaryIntervalSecs = 300;
+  static constexpr uint32_t kLiveOnlyIdleTimeoutMs = 2UL * 60UL * 1000UL;
 
   StatsHistory();
   ~StatsHistory();
@@ -77,6 +78,7 @@ public:
   bool isRecentHistoryAvailable() const { return _samples != nullptr; }
   bool isPsramBacked() const { return _psram_backed; }
   bool isDegraded() const { return _degraded; }
+  bool isLiveOnly() const { return _live_only; }
   bool isArchiveAvailable() const;
   bool hasArchiveRestore() const { return _restored_sample_count > 0; }
 
@@ -89,6 +91,8 @@ public:
   void pushSample(const HistorySample& sample);
   void recordEvent(uint8_t type, uint32_t epoch_secs, uint32_t uptime_secs, int16_t value = 0);
   void maybeFlush(uint32_t now_ms);
+  void noteAccess(uint32_t now_ms);
+  void maybeReleaseIdleBuffers(uint32_t now_ms);
 
   bool buildSeriesJson(const char* series, char* buffer, size_t buffer_size, uint32_t now_epoch_secs, uint32_t now_uptime_secs) const;
   bool getRecentEvent(size_t reverse_index, HistoryEvent& event) const;
@@ -98,7 +102,11 @@ public:
   static constexpr uint32_t getArchiveSummaryIntervalSecs() { return kArchiveSummaryIntervalSecs; }
 
 private:
+  bool activate();
   bool ensureBuffers();
+  void releaseBuffers();
+  bool supportsPersistence() const;
+  bool isAccessActive(uint32_t now_ms) const;
   bool restoreSummaryLog();
   bool restoreEventsLog();
   bool parseSummaryLine(const char* line, HistorySample& sample) const;
@@ -112,11 +120,14 @@ private:
   bool getSampleFromOldest(size_t index, HistorySample& sample) const;
 
   bool _enabled;
+  bool _activated;
   bool _psram_backed;
   bool _degraded;
+  bool _live_only;
   bool _summary_dirty;
   uint32_t _next_summary_flush_ms;
   uint32_t _next_event_flush_ms;
+  uint32_t _last_access_ms;
   size_t _sample_capacity;
   size_t _sample_head;
   size_t _sample_count;