Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .clang-format
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
BasedOnStyle: LLVM
ColumnLimit: 100
BinPackArguments: false
BinPackParameters: false
AllowAllArgumentsOnNextLine: false
AlignAfterOpenBracket: BlockIndent
UseTab: ForIndentation
IndentWidth: 4
TabWidth: 4
ContinuationIndentWidth: 4
AllowShortFunctionsOnASingleLine: None
11 changes: 11 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
root = true

[*]
end_of_line = lf
insert_final_newline = true
charset = utf-8

[*.{c,cc,cpp,h,hpp,ino}]
indent_style = tab
indent_size = tab
tab_width = 4
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
.venv
build/
build_prev_runner/
.vscode
19 changes: 19 additions & 0 deletions .vscode/bin/clang-format
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#!/usr/bin/env bash

set -euo pipefail

if command -v clang-format >/dev/null 2>&1; then
exec clang-format "$@"
fi

_home_dir="${HOME:-}"
if [ -n "$_home_dir" ]; then
_candidate="$(ls -1d "$_home_dir"/.vscode/extensions/ms-vscode.cpptools-*-linux-x64/LLVM/bin/clang-format 2>/dev/null | tail -n 1 || true)"
if [ -n "$_candidate" ] && [ -x "$_candidate" ]; then
exec "$_candidate" "$@"
fi
fi

echo "clang-format executable not found." >&2
echo "Install clang-format system-wide or install/update ms-vscode.cpptools." >&2
exit 127
9 changes: 9 additions & 0 deletions .vscode/extensions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"recommendations": [
"pioarduino.pioarduino-ide",
"xaver.clang-format"
],
"unwantedRecommendations": [
"ms-vscode.cpptools-extension-pack"
]
}
30 changes: 30 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"files.associations": {
"*.ino": "cpp"
},
"editor.defaultFormatter": "xaver.clang-format",
"C_Cpp.formatting": "Disabled",
"clang-format.style": "file",
"clang-format.executable": "${workspaceRoot}/.vscode/bin/clang-format",
"[cpp]": {
"editor.defaultFormatter": "xaver.clang-format",
"editor.detectIndentation": false,
"editor.insertSpaces": false,
"editor.tabSize": 4,
"editor.formatOnSave": true
},
"[c]": {
"editor.defaultFormatter": "xaver.clang-format",
"editor.detectIndentation": false,
"editor.insertSpaces": false,
"editor.tabSize": 4,
"editor.formatOnSave": true
},
"[arduino]": {
"editor.defaultFormatter": "xaver.clang-format",
"editor.detectIndentation": false,
"editor.insertSpaces": false,
"editor.tabSize": 4,
"editor.formatOnSave": true
}
}
12 changes: 12 additions & 0 deletions .vscode/tasks.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "Format Firmware Sources",
"type": "shell",
"command": "bash ${workspaceFolder}/scripts/format_cpp.sh",
"group": "build",
"problemMatcher": []
}
]
}
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,13 @@ serializeJson(doc, Serial);
- Lifecycle teardown tests are available in `test/test_memory_monitor_lifecycle` (pre-init `deinit()`, idempotent `deinit()`, re-init, and destructor teardown behavior).
- Host-side tests are disabled because this library depends on ESP-IDF/FreeRTOS runtime APIs; run the lifecycle suite on device with PlatformIO/Arduino.

## Formatting Baseline

This repository follows the firmware formatting baseline from `esptoolkit-template`:
- `.clang-format` is the source of truth for C/C++/INO layout.
- `.editorconfig` enforces tabs (`tab_width = 4`), LF endings, and final newline.
- Format all tracked firmware sources with `bash scripts/format_cpp.sh`.

## License
MIT — see [LICENSE.md](LICENSE.md).

Expand Down
209 changes: 129 additions & 80 deletions examples/basic_monitor/basic_monitor.ino
Original file line number Diff line number Diff line change
Expand Up @@ -6,97 +6,146 @@ ESPMemoryMonitor monitor;
MemoryTag httpTag;

void setup() {
Serial.begin(115200);
Serial.begin(115200);

MemoryMonitorConfig cfg;
cfg.sampleIntervalMs = 1000;
cfg.historySize = 30;
cfg.internal = {48 * 1024, 32 * 1024};
cfg.psram = {256 * 1024, 160 * 1024};
cfg.enablePerTaskStacks = true;
cfg.enableFailedAllocEvents = true;
cfg.enableScopes = true;
cfg.maxScopesInHistory = 16;
cfg.windowStatsSize = 8;
cfg.enableTaskTracking = true;
monitor.init(cfg);
MemoryMonitorConfig cfg;
cfg.sampleIntervalMs = 1000;
cfg.historySize = 30;
cfg.internal = {48 * 1024, 32 * 1024};
cfg.psram = {256 * 1024, 160 * 1024};
cfg.enablePerTaskStacks = true;
cfg.enableFailedAllocEvents = true;
cfg.enableScopes = true;
cfg.maxScopesInHistory = 16;
cfg.windowStatsSize = 8;
cfg.enableTaskTracking = true;
monitor.init(cfg);

httpTag = monitor.registerTag("http_server");
monitor.setTagBudget(httpTag, {60 * 1024, 80 * 1024});
httpTag = monitor.registerTag("http_server");
monitor.setTagBudget(httpTag, {60 * 1024, 80 * 1024});

monitor.onThreshold([](const ThresholdEvent &evt) {
const char *region = evt.region == MemoryRegion::Psram ? "PSRAM" : "DRAM";
if (evt.state == ThresholdState::Critical) {
ESP_LOGE("MEM", "%s critical free=%u", region, static_cast<unsigned>(evt.stats.freeBytes));
} else if (evt.state == ThresholdState::Warn) {
ESP_LOGW("MEM", "%s warning free=%u", region, static_cast<unsigned>(evt.stats.freeBytes));
} else {
ESP_LOGI("MEM", "%s recovered free=%u", region, static_cast<unsigned>(evt.stats.freeBytes));
}
});
monitor.onThreshold([](const ThresholdEvent &evt) {
const char *region = evt.region == MemoryRegion::Psram ? "PSRAM" : "DRAM";
if (evt.state == ThresholdState::Critical) {
ESP_LOGE(
"MEM",
"%s critical free=%u",
region,
static_cast<unsigned>(evt.stats.freeBytes)
);
} else if (evt.state == ThresholdState::Warn) {
ESP_LOGW(
"MEM",
"%s warning free=%u",
region,
static_cast<unsigned>(evt.stats.freeBytes)
);
} else {
ESP_LOGI(
"MEM",
"%s recovered free=%u",
region,
static_cast<unsigned>(evt.stats.freeBytes)
);
}
});

monitor.onSample([](const MemorySnapshot &snapshot) {
for (const auto &region : snapshot.regions) {
const char *regionName = region.region == MemoryRegion::Psram ? "PSRAM" : "DRAM";
ESP_LOGI("MEM", "%s free=%u min=%u frag=%.02f", regionName,
static_cast<unsigned>(region.freeBytes),
static_cast<unsigned>(region.minimumFreeBytes),
region.fragmentation);
}
for (const auto &stack : snapshot.stacks) {
ESP_LOGD("STACK", "%s water=%uB priority=%u state=%d", stack.name.c_str(),
static_cast<unsigned>(stack.freeHighWaterBytes), stack.priority, static_cast<int>(stack.state));
}
});
monitor.onSample([](const MemorySnapshot &snapshot) {
for (const auto &region : snapshot.regions) {
const char *regionName = region.region == MemoryRegion::Psram ? "PSRAM" : "DRAM";
ESP_LOGI(
"MEM",
"%s free=%u min=%u frag=%.02f",
regionName,
static_cast<unsigned>(region.freeBytes),
static_cast<unsigned>(region.minimumFreeBytes),
region.fragmentation
);
}
for (const auto &stack : snapshot.stacks) {
ESP_LOGD(
"STACK",
"%s water=%uB priority=%u state=%d",
stack.name.c_str(),
static_cast<unsigned>(stack.freeHighWaterBytes),
stack.priority,
static_cast<int>(stack.state)
);
}
});

monitor.onFailedAlloc([](const FailedAllocEvent &evt) {
ESP_LOGE("MEM", "alloc failed size=%u caps=0x%08x from %s", static_cast<unsigned>(evt.requestedBytes), evt.caps, evt.functionName);
});
monitor.onFailedAlloc([](const FailedAllocEvent &evt) {
ESP_LOGE(
"MEM",
"alloc failed size=%u caps=0x%08x from %s",
static_cast<unsigned>(evt.requestedBytes),
evt.caps,
evt.functionName
);
});

monitor.onScope([](const ScopeStats &s) {
ESP_LOGI("SCOPE", "%s used %+d DRAM %+d PSRAM in %llu us",
s.name.c_str(),
static_cast<int>(s.deltaInternalBytes),
static_cast<int>(s.deltaPsramBytes),
static_cast<unsigned long long>(s.durationUs));
});
monitor.onScope([](const ScopeStats &s) {
ESP_LOGI(
"SCOPE",
"%s used %+d DRAM %+d PSRAM in %llu us",
s.name.c_str(),
static_cast<int>(s.deltaInternalBytes),
static_cast<int>(s.deltaPsramBytes),
static_cast<unsigned long long>(s.durationUs)
);
});

monitor.onTagThreshold([](const TagThresholdEvent &evt) {
ESP_LOGW("TAG", "%s now %s at %u bytes",
evt.usage.name.c_str(),
evt.usage.state == ThresholdState::Critical ? "CRITICAL" :
evt.usage.state == ThresholdState::Warn ? "WARN" : "OK",
static_cast<unsigned>(evt.usage.totalInternalBytes + evt.usage.totalPsramBytes));
});
monitor.onTagThreshold([](const TagThresholdEvent &evt) {
ESP_LOGW(
"TAG",
"%s now %s at %u bytes",
evt.usage.name.c_str(),
evt.usage.state == ThresholdState::Critical ? "CRITICAL"
: evt.usage.state == ThresholdState::Warn ? "WARN"
: "OK",
static_cast<unsigned>(evt.usage.totalInternalBytes + evt.usage.totalPsramBytes)
);
});

monitor.onTaskStackThreshold([](const TaskStackEvent &evt) {
if (evt.appeared) {
ESP_LOGI("TASK", "task created: %s", evt.usage.name.c_str());
} else if (evt.disappeared) {
ESP_LOGW("TASK", "task disappeared: %s", evt.usage.name.c_str());
} else {
ESP_LOGW("TASK", "task %s stack %s (%uB headroom)", evt.usage.name.c_str(),
evt.state == StackState::Critical ? "CRITICAL" :
evt.state == StackState::Warn ? "WARN" : "SAFE",
static_cast<unsigned>(evt.usage.freeHighWaterBytes));
}
});
monitor.onTaskStackThreshold([](const TaskStackEvent &evt) {
if (evt.appeared) {
ESP_LOGI("TASK", "task created: %s", evt.usage.name.c_str());
} else if (evt.disappeared) {
ESP_LOGW("TASK", "task disappeared: %s", evt.usage.name.c_str());
} else {
ESP_LOGW(
"TASK",
"task %s stack %s (%uB headroom)",
evt.usage.name.c_str(),
evt.state == StackState::Critical ? "CRITICAL"
: evt.state == StackState::Warn ? "WARN"
: "SAFE",
static_cast<unsigned>(evt.usage.freeHighWaterBytes)
);
}
});

monitor.onLeakCheck([](const LeakCheckResult &res) {
for (const auto &d : res.deltas) {
const char *regionName = d.region == MemoryRegion::Psram ? "PSRAM" : "DRAM";
ESP_LOGI("LEAK", "%s drift %+0.1fB frag %+0.2f", regionName, d.deltaFreeBytes, d.deltaFragmentation);
}
});
monitor.onLeakCheck([](const LeakCheckResult &res) {
for (const auto &d : res.deltas) {
const char *regionName = d.region == MemoryRegion::Psram ? "PSRAM" : "DRAM";
ESP_LOGI(
"LEAK",
"%s drift %+0.1fB frag %+0.2f",
regionName,
d.deltaFreeBytes,
d.deltaFragmentation
);
}
});
}

void loop() {
auto scope = monitor.beginScope("http_req", httpTag);
delay(1000);
scope.end();
auto scope = monitor.beginScope("http_req", httpTag);
delay(1000);
scope.end();

static uint32_t counter = 0;
if (++counter % 60 == 0) {
monitor.markLeakCheckPoint("steady_state");
}
static uint32_t counter = 0;
if (++counter % 60 == 0) {
monitor.markLeakCheckPoint("steady_state");
}
}
Loading