Ram Monitor

Source: src/ram_monitor.c · 196 lines · 9 functions.

Module entry points

These functions are exposed to the rest of the project. Open one to follow its decisions and the helpers it calls.

How to read these diagrams

Each function has its own expandable diagram using the actual expressions and statements from this file. Decisions split into True and False; loop bodies are indented. Returns end the function immediately; break and continue follow their C meanings. Arrows between statements show their order when execution reaches them.

These are structural source diagrams before preprocessing, not runtime traces. Conditional compilation and macros remain source expressions. Ternary choices remain inside their statement. Calls are linked separately; callback execution and task scheduling are not inferred.

Functions in this file

captureSnapshot() · source line 38
static void captureSnapshot(RamSnapshot *snapshot)
  1. Executeconst uint32_t internal_caps = MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT;
  2. Executesnapshot->psram_free = heap_caps_get_free_size(MALLOC_CAP_SPIRAM);
  3. Executesnapshot->psram_min = heap_caps_get_minimum_free_size(MALLOC_CAP_SPIRAM);
  4. Executesnapshot->psram_largest = heap_caps_get_largest_free_block(MALLOC_CAP_SPIRAM);
  5. Executesnapshot->heap_free = heap_caps_get_free_size(internal_caps);
  6. Executesnapshot->heap_min = heap_caps_get_minimum_free_size(internal_caps);
  7. Executesnapshot->heap_largest = heap_caps_get_largest_free_block(internal_caps);
  8. Executesnapshot->stack_min = (size_t)uxTaskGetStackHighWaterMark(NULL) * sizeof(StackType_t);
Read original function source
static void captureSnapshot(RamSnapshot *snapshot)
{
    const uint32_t internal_caps = MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT;
    snapshot->psram_free = heap_caps_get_free_size(MALLOC_CAP_SPIRAM);
    snapshot->psram_min = heap_caps_get_minimum_free_size(MALLOC_CAP_SPIRAM);
    snapshot->psram_largest = heap_caps_get_largest_free_block(MALLOC_CAP_SPIRAM);
    snapshot->heap_free = heap_caps_get_free_size(internal_caps);
    snapshot->heap_min = heap_caps_get_minimum_free_size(internal_caps);
    snapshot->heap_largest = heap_caps_get_largest_free_block(internal_caps);
    snapshot->stack_min = (size_t)uxTaskGetStackHighWaterMark(NULL) * sizeof(StackType_t);
}
snapshotIsWarning() · source line 50
static bool snapshotIsWarning(const RamSnapshot *snapshot)
  1. Exit this functionreturn snapshot->heap_free < RAM_HEAP_WARNING_BYTES || snapshot->psram_free < RAM_PSRAM_WARNING_BYTES || snapshot->stack_min < RAM_STACK_WARNING_BYTES;
Read original function source
static bool snapshotIsWarning(const RamSnapshot *snapshot)
{
    return snapshot->heap_free < RAM_HEAP_WARNING_BYTES ||
           snapshot->psram_free < RAM_PSRAM_WARNING_BYTES ||
           snapshot->stack_min < RAM_STACK_WARNING_BYTES;
}
countRecords() · source line 57
static unsigned countRecords(FILE *file)
  1. Executechar line[RAM_LOG_LINE_CHARS];
  2. Decisionfgets(line, sizeof(line), file) == NULL
    True
    1. Exit this functionreturn 0U;
    False

    Continue after this decision.

  3. Executeunsigned records = 0U;
  4. WHILE loopfgets(line, sizeof(line), file) != NULL

    Repeat body while the loop condition permits; then continue below.

      1. Decisionline[0] != '\0' && line[0] != '\n'
        True
        1. Execute++records;
        False

        Continue after this decision.

  5. Exit this functionreturn records;
Read original function source
static unsigned countRecords(FILE *file)
{
    char line[RAM_LOG_LINE_CHARS];
    if (fgets(line, sizeof(line), file) == NULL) return 0U;

    unsigned records = 0U;
    while (fgets(line, sizeof(line), file) != NULL) {
        if (line[0] != '\0' && line[0] != '\n') ++records;
    }
    return records;
}
ramMonitorRecordCount() · source line 69
unsigned ramMonitorRecordCount(void)

Calls into this documentation:

  1. ExecuteFILE *file = fopen(RAM_LOG_PATH, "rb");
  2. Decisionfile == NULL
    True
    1. Exit this functionreturn 0U;
    False

    Continue after this decision.

  3. Executeconst unsigned records = countRecords(file);
  4. Executefclose(file);
  5. Exit this functionreturn records;
Read original function source
unsigned ramMonitorRecordCount(void)
{
    FILE *file = fopen(RAM_LOG_PATH, "rb");
    if (file == NULL) return 0U;
    const unsigned records = countRecords(file);
    fclose(file);
    return records;
}
ramMonitorReadRecord() · source line 78
bool ramMonitorReadRecord(unsigned record_index, char *record, size_t record_size)
  1. Decisionrecord == NULL || record_size == 0U
    True
    1. Exit this functionreturn false;
    False

    Continue after this decision.

  2. Executerecord[0] = '\0';
  3. ExecuteFILE *file = fopen(RAM_LOG_PATH, "rb");
  4. Decisionfile == NULL
    True
    1. Exit this functionreturn false;
    False

    Continue after this decision.

  5. Executechar line[RAM_LOG_LINE_CHARS];
  6. Decisionfgets(line, sizeof(line), file) == NULL
    True
      1. Executefclose(file);
      2. Exit this functionreturn false;
    False

    Continue after this decision.

  7. Executeunsigned index = 0U;
  8. WHILE loopfgets(line, sizeof(line), file) != NULL

    Repeat body while the loop condition permits; then continue below.

      1. Decisionline[0] == '\0' || line[0] == '\n'
        True
        1. Next loop iterationcontinue;
        False

        Continue after this decision.

      2. Decisionindex++ != record_index
        True
        1. Next loop iterationcontinue;
        False

        Continue after this decision.

      3. Executesize_t length = strlen(line);
      4. WHILE looplength > 0U && (line[length - 1U] == '\n' || line[length - 1U] == '\r')

        Repeat body while the loop condition permits; then continue below.

        1. Execute--length;
      5. Decisionlength >= record_size
        True
        1. Executelength = record_size - 1U;
        False

        Continue after this decision.

      6. Executememcpy(record, line, length);
      7. Executerecord[length] = '\0';
      8. Executefclose(file);
      9. Exit this functionreturn true;
  9. Executefclose(file);
  10. Exit this functionreturn false;
Read original function source
bool ramMonitorReadRecord(unsigned record_index, char *record, size_t record_size)
{
    if (record == NULL || record_size == 0U) return false;
    record[0] = '\0';
    FILE *file = fopen(RAM_LOG_PATH, "rb");
    if (file == NULL) return false;

    char line[RAM_LOG_LINE_CHARS];
    if (fgets(line, sizeof(line), file) == NULL) {
        fclose(file);
        return false;
    }
    unsigned index = 0U;
    while (fgets(line, sizeof(line), file) != NULL) {
        if (line[0] == '\0' || line[0] == '\n') continue;
        if (index++ != record_index) continue;
        size_t length = strlen(line);
        while (length > 0U && (line[length - 1U] == '\n' || line[length - 1U] == '\r')) --length;
        if (length >= record_size) length = record_size - 1U;
        memcpy(record, line, length);
        record[length] = '\0';
        fclose(file);
        return true;
    }
    fclose(file);
    return false;
}
appendSnapshot() · source line 106
static bool appendSnapshot(const char *record)

Calls into this documentation:

  1. ExecuteFILE *source = fopen(RAM_LOG_PATH, "rb");
  2. Decisionsource == NULL
    True
      1. ExecuteFILE *file = fopen(RAM_LOG_PATH, "wb");
      2. Decisionfile == NULL
        True
        1. Exit this functionreturn false;
        False

        Continue after this decision.

      3. Executeconst bool written = fprintf(file, "# Mini Arcade RAM monitor; F=free M=minimum X=largest S=stack\n%s\n", record) > 0;
      4. Exit this functionreturn fclose(file) == 0 && written;
    False

    Continue after this decision.

  3. Executeconst unsigned records = countRecords(source);
  4. Executefclose(source);
  5. Decisionrecords < RAM_LOG_MAX_RECORDS
    True
      1. ExecuteFILE *file = fopen(RAM_LOG_PATH, "ab");
      2. Decisionfile == NULL
        True
        1. Exit this functionreturn false;
        False

        Continue after this decision.

      3. Executeconst bool written = fprintf(file, "%s\n", record) > 0;
      4. Exit this functionreturn fclose(file) == 0 && written;
    False

    Continue after this decision.

  6. Executesource = fopen(RAM_LOG_PATH, "rb");
  7. ExecuteFILE *temporary = fopen(RAM_LOG_TEMP_PATH, "wb");
  8. Decisionsource == NULL || temporary == NULL
    True
      1. Decisionsource != NULL
        True
        1. Executefclose(source);
        False

        Continue after this decision.

      2. Decisiontemporary != NULL
        True
        1. Executefclose(temporary);
        False

        Continue after this decision.

      3. Executeremove(RAM_LOG_TEMP_PATH);
      4. Exit this functionreturn false;
    False

    Continue after this decision.

  9. Executechar line[RAM_LOG_LINE_CHARS];
  10. Executebool written = fgets(line, sizeof(line), source) != NULL && fputs(line, temporary) >= 0;
  11. Executeunsigned skipped = 0U;
  12. WHILE loopwritten && skipped <= records - RAM_LOG_MAX_RECORDS && fgets(line, sizeof(line), source) != NULL

    Repeat body while the loop condition permits; then continue below.

      1. Execute++skipped;
  13. WHILE loopwritten && fgets(line, sizeof(line), source) != NULL

    Repeat body while the loop condition permits; then continue below.

    1. Executewritten = fputs(line, temporary) >= 0;
  14. Decisionwritten
    True
    1. Executewritten = fprintf(temporary, "%s\n", record) > 0;
    False

    Continue after this decision.

  15. Decisionfclose(source) != 0
    True
    1. Executewritten = false;
    False

    Continue after this decision.

  16. Decisionfclose(temporary) != 0
    True
    1. Executewritten = false;
    False

    Continue after this decision.

  17. Decision!written || rename(RAM_LOG_TEMP_PATH, RAM_LOG_PATH) != 0
    True
      1. Executeremove(RAM_LOG_TEMP_PATH);
      2. Exit this functionreturn false;
    False

    Continue after this decision.

  18. Exit this functionreturn true;
Read original function source
static bool appendSnapshot(const char *record)
{
    FILE *source = fopen(RAM_LOG_PATH, "rb");
    if (source == NULL) {
        FILE *file = fopen(RAM_LOG_PATH, "wb");
        if (file == NULL) return false;
        const bool written = fprintf(file, "# Mini Arcade RAM monitor; F=free M=minimum X=largest S=stack\n%s\n",
                                     record) > 0;
        return fclose(file) == 0 && written;
    }

    const unsigned records = countRecords(source);
    fclose(source);
    if (records < RAM_LOG_MAX_RECORDS) {
        FILE *file = fopen(RAM_LOG_PATH, "ab");
        if (file == NULL) return false;
        const bool written = fprintf(file, "%s\n", record) > 0;
        return fclose(file) == 0 && written;
    }

    source = fopen(RAM_LOG_PATH, "rb");
    FILE *temporary = fopen(RAM_LOG_TEMP_PATH, "wb");
    if (source == NULL || temporary == NULL) {
        if (source != NULL) fclose(source);
        if (temporary != NULL) fclose(temporary);
        remove(RAM_LOG_TEMP_PATH);
        return false;
    }

    char line[RAM_LOG_LINE_CHARS];
    bool written = fgets(line, sizeof(line), source) != NULL && fputs(line, temporary) >= 0;
    unsigned skipped = 0U;
    while (written && skipped <= records - RAM_LOG_MAX_RECORDS &&
           fgets(line, sizeof(line), source) != NULL) {
        ++skipped;
    }
    while (written && fgets(line, sizeof(line), source) != NULL)
        written = fputs(line, temporary) >= 0;
    if (written) written = fprintf(temporary, "%s\n", record) > 0;
    if (fclose(source) != 0) written = false;
    if (fclose(temporary) != 0) written = false;
    if (!written || rename(RAM_LOG_TEMP_PATH, RAM_LOG_PATH) != 0) {
        remove(RAM_LOG_TEMP_PATH);
        return false;
    }
    return true;
}
persistSnapshot() · source line 154
static bool persistSnapshot(const RamSnapshot *snapshot, uint32_t now_ms, bool warning)

Calls into this documentation:

  1. Executechar record[RAM_LOG_LINE_CHARS];
  2. Executeconst int length = snprintf(record, sizeof(record), "UP=%lus PF=%uK PM=%uK PX=%uK HF=%uK HM=%uK HX=%uK SM=%uK%s", (unsigned long)(now_ms / 1000U), (unsigned)(snapshot->psram_free / 1024U), (unsigned)(snapshot->psram_min / 1024U), (unsigned)(snapshot->psram_largest / 1024U), (unsigned)(snapshot->heap_free / 1024U), (unsigned)(snapshot->heap_min / 1024U), (unsigned)(snapshot->heap_largest / 1024U), (unsigned)(snapshot->stack_min / 1024U), warning ? " WARN" : "");
  3. Decisionlength < 0 || (size_t)length >= sizeof(record)
    True
    1. Exit this functionreturn false;
    False

    Continue after this decision.

  4. Decision!appendSnapshot(record)
    True
    1. Exit this functionreturn false;
    False

    Continue after this decision.

  5. ExecuteESP_LOGI(TAG, "%s", record);
  6. Exit this functionreturn true;
Read original function source
static bool persistSnapshot(const RamSnapshot *snapshot, uint32_t now_ms, bool warning)
{
    char record[RAM_LOG_LINE_CHARS];
    const int length = snprintf(record, sizeof(record),
                                "UP=%lus PF=%uK PM=%uK PX=%uK HF=%uK HM=%uK HX=%uK SM=%uK%s",
                                (unsigned long)(now_ms / 1000U),
                                (unsigned)(snapshot->psram_free / 1024U),
                                (unsigned)(snapshot->psram_min / 1024U),
                                (unsigned)(snapshot->psram_largest / 1024U),
                                (unsigned)(snapshot->heap_free / 1024U),
                                (unsigned)(snapshot->heap_min / 1024U),
                                (unsigned)(snapshot->heap_largest / 1024U),
                                (unsigned)(snapshot->stack_min / 1024U), warning ? " WARN" : "");
    if (length < 0 || (size_t)length >= sizeof(record)) return false;
    if (!appendSnapshot(record)) return false;
    ESP_LOGI(TAG, "%s", record);
    return true;
}
ramMonitorInitialize() · source line 173
void ramMonitorInitialize(void)
  1. Executelast_sample_ms = 0U;
  2. Executelast_persist_ms = 0U;
  3. Executewarning_active = false;
  4. ExecuteESP_LOGI(TAG, "RAM monitor ready: sample 30s, persist 60s, log %u records", RAM_LOG_MAX_RECORDS);
Read original function source
void ramMonitorInitialize(void)
{
    last_sample_ms = 0U;
    last_persist_ms = 0U;
    warning_active = false;
    ESP_LOGI(TAG, "RAM monitor ready: sample 30s, persist 60s, log %u records", RAM_LOG_MAX_RECORDS);
}
ramMonitorUpdate() · source line 181
void ramMonitorUpdate(uint32_t now_ms)

Calls into this documentation:

  1. Decision(uint32_t)(now_ms - last_sample_ms) < RAM_SAMPLE_INTERVAL_MS
    True
    1. Exit this functionreturn;
    False

    Continue after this decision.

  2. Executelast_sample_ms = now_ms;
  3. ExecuteRamSnapshot snapshot;
  4. ExecutecaptureSnapshot(&snapshot);
  5. Executeconst bool warning = snapshotIsWarning(&snapshot);
  6. Executeconst bool warning_started = warning && !warning_active;
  7. Executewarning_active = warning;
  8. Executeconst bool periodic_due = (uint32_t)(now_ms - last_persist_ms) >= RAM_PERSIST_INTERVAL_MS;
  9. Decision!periodic_due && !warning_started
    True
    1. Exit this functionreturn;
    False

    Continue after this decision.

  10. DecisionpersistSnapshot(&snapshot, now_ms, warning)
    True
    1. Executelast_persist_ms = now_ms;
    False
    1. ExecuteESP_LOGE(TAG, "Could not write %s", RAM_LOG_PATH);
Read original function source
void ramMonitorUpdate(uint32_t now_ms)
{
    if ((uint32_t)(now_ms - last_sample_ms) < RAM_SAMPLE_INTERVAL_MS) return;
    last_sample_ms = now_ms;

    RamSnapshot snapshot;
    captureSnapshot(&snapshot);
    const bool warning = snapshotIsWarning(&snapshot);
    const bool warning_started = warning && !warning_active;
    warning_active = warning;
    const bool periodic_due = (uint32_t)(now_ms - last_persist_ms) >= RAM_PERSIST_INTERVAL_MS;
    if (!periodic_due && !warning_started) return;

    if (persistSnapshot(&snapshot, now_ms, warning)) last_persist_ms = now_ms;
    else ESP_LOGE(TAG, "Could not write %s", RAM_LOG_PATH);
}
Complete original file, including constants and data
/* Coded By JD(2026) */
#error "Reference listing only — not provided as build-ready source."
#include "ram_monitor.h"

#include <stdbool.h>
#include <stdio.h>
#include <string.h>

#include "esp_heap_caps.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"

#define RAM_LOG_PATH "/spiffs/ram_log.txt"
#define RAM_LOG_TEMP_PATH "/spiffs/ram_log.tmp"
#define RAM_SAMPLE_INTERVAL_MS 30000U
#define RAM_PERSIST_INTERVAL_MS 60000U
#define RAM_LOG_MAX_RECORDS 128U
#define RAM_LOG_LINE_CHARS RAM_MONITOR_RECORD_CHARS
#define RAM_HEAP_WARNING_BYTES (32U * 1024U)
#define RAM_PSRAM_WARNING_BYTES (512U * 1024U)
#define RAM_STACK_WARNING_BYTES (2U * 1024U)

static const char *TAG = "ram_monitor";

typedef struct {
    size_t psram_free;
    size_t psram_min;
    size_t psram_largest;
    size_t heap_free;
    size_t heap_min;
    size_t heap_largest;
    size_t stack_min;
} RamSnapshot;

static uint32_t last_sample_ms;
static uint32_t last_persist_ms;
static bool warning_active;

static void captureSnapshot(RamSnapshot *snapshot)
{
    const uint32_t internal_caps = MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT;
    snapshot->psram_free = heap_caps_get_free_size(MALLOC_CAP_SPIRAM);
    snapshot->psram_min = heap_caps_get_minimum_free_size(MALLOC_CAP_SPIRAM);
    snapshot->psram_largest = heap_caps_get_largest_free_block(MALLOC_CAP_SPIRAM);
    snapshot->heap_free = heap_caps_get_free_size(internal_caps);
    snapshot->heap_min = heap_caps_get_minimum_free_size(internal_caps);
    snapshot->heap_largest = heap_caps_get_largest_free_block(internal_caps);
    snapshot->stack_min = (size_t)uxTaskGetStackHighWaterMark(NULL) * sizeof(StackType_t);
}

static bool snapshotIsWarning(const RamSnapshot *snapshot)
{
    return snapshot->heap_free < RAM_HEAP_WARNING_BYTES ||
           snapshot->psram_free < RAM_PSRAM_WARNING_BYTES ||
           snapshot->stack_min < RAM_STACK_WARNING_BYTES;
}

static unsigned countRecords(FILE *file)
{
    char line[RAM_LOG_LINE_CHARS];
    if (fgets(line, sizeof(line), file) == NULL) return 0U;

    unsigned records = 0U;
    while (fgets(line, sizeof(line), file) != NULL) {
        if (line[0] != '\0' && line[0] != '\n') ++records;
    }
    return records;
}

unsigned ramMonitorRecordCount(void)
{
    FILE *file = fopen(RAM_LOG_PATH, "rb");
    if (file == NULL) return 0U;
    const unsigned records = countRecords(file);
    fclose(file);
    return records;
}

bool ramMonitorReadRecord(unsigned record_index, char *record, size_t record_size)
{
    if (record == NULL || record_size == 0U) return false;
    record[0] = '\0';
    FILE *file = fopen(RAM_LOG_PATH, "rb");
    if (file == NULL) return false;

    char line[RAM_LOG_LINE_CHARS];
    if (fgets(line, sizeof(line), file) == NULL) {
        fclose(file);
        return false;
    }
    unsigned index = 0U;
    while (fgets(line, sizeof(line), file) != NULL) {
        if (line[0] == '\0' || line[0] == '\n') continue;
        if (index++ != record_index) continue;
        size_t length = strlen(line);
        while (length > 0U && (line[length - 1U] == '\n' || line[length - 1U] == '\r')) --length;
        if (length >= record_size) length = record_size - 1U;
        memcpy(record, line, length);
        record[length] = '\0';
        fclose(file);
        return true;
    }
    fclose(file);
    return false;
}

static bool appendSnapshot(const char *record)
{
    FILE *source = fopen(RAM_LOG_PATH, "rb");
    if (source == NULL) {
        FILE *file = fopen(RAM_LOG_PATH, "wb");
        if (file == NULL) return false;
        const bool written = fprintf(file, "# Mini Arcade RAM monitor; F=free M=minimum X=largest S=stack\n%s\n",
                                     record) > 0;
        return fclose(file) == 0 && written;
    }

    const unsigned records = countRecords(source);
    fclose(source);
    if (records < RAM_LOG_MAX_RECORDS) {
        FILE *file = fopen(RAM_LOG_PATH, "ab");
        if (file == NULL) return false;
        const bool written = fprintf(file, "%s\n", record) > 0;
        return fclose(file) == 0 && written;
    }

    source = fopen(RAM_LOG_PATH, "rb");
    FILE *temporary = fopen(RAM_LOG_TEMP_PATH, "wb");
    if (source == NULL || temporary == NULL) {
        if (source != NULL) fclose(source);
        if (temporary != NULL) fclose(temporary);
        remove(RAM_LOG_TEMP_PATH);
        return false;
    }

    char line[RAM_LOG_LINE_CHARS];
    bool written = fgets(line, sizeof(line), source) != NULL && fputs(line, temporary) >= 0;
    unsigned skipped = 0U;
    while (written && skipped <= records - RAM_LOG_MAX_RECORDS &&
           fgets(line, sizeof(line), source) != NULL) {
        ++skipped;
    }
    while (written && fgets(line, sizeof(line), source) != NULL)
        written = fputs(line, temporary) >= 0;
    if (written) written = fprintf(temporary, "%s\n", record) > 0;
    if (fclose(source) != 0) written = false;
    if (fclose(temporary) != 0) written = false;
    if (!written || rename(RAM_LOG_TEMP_PATH, RAM_LOG_PATH) != 0) {
        remove(RAM_LOG_TEMP_PATH);
        return false;
    }
    return true;
}

static bool persistSnapshot(const RamSnapshot *snapshot, uint32_t now_ms, bool warning)
{
    char record[RAM_LOG_LINE_CHARS];
    const int length = snprintf(record, sizeof(record),
                                "UP=%lus PF=%uK PM=%uK PX=%uK HF=%uK HM=%uK HX=%uK SM=%uK%s",
                                (unsigned long)(now_ms / 1000U),
                                (unsigned)(snapshot->psram_free / 1024U),
                                (unsigned)(snapshot->psram_min / 1024U),
                                (unsigned)(snapshot->psram_largest / 1024U),
                                (unsigned)(snapshot->heap_free / 1024U),
                                (unsigned)(snapshot->heap_min / 1024U),
                                (unsigned)(snapshot->heap_largest / 1024U),
                                (unsigned)(snapshot->stack_min / 1024U), warning ? " WARN" : "");
    if (length < 0 || (size_t)length >= sizeof(record)) return false;
    if (!appendSnapshot(record)) return false;
    ESP_LOGI(TAG, "%s", record);
    return true;
}

void ramMonitorInitialize(void)
{
    last_sample_ms = 0U;
    last_persist_ms = 0U;
    warning_active = false;
    ESP_LOGI(TAG, "RAM monitor ready: sample 30s, persist 60s, log %u records", RAM_LOG_MAX_RECORDS);
}

void ramMonitorUpdate(uint32_t now_ms)
{
    if ((uint32_t)(now_ms - last_sample_ms) < RAM_SAMPLE_INTERVAL_MS) return;
    last_sample_ms = now_ms;

    RamSnapshot snapshot;
    captureSnapshot(&snapshot);
    const bool warning = snapshotIsWarning(&snapshot);
    const bool warning_started = warning && !warning_active;
    warning_active = warning;
    const bool periodic_due = (uint32_t)(now_ms - last_persist_ms) >= RAM_PERSIST_INTERVAL_MS;
    if (!periodic_due && !warning_started) return;

    if (persistSnapshot(&snapshot, now_ms, warning)) last_persist_ms = now_ms;
    else ESP_LOGE(TAG, "Could not write %s", RAM_LOG_PATH);
}