WiFiManager: stabilize root rendering and reduce page reallocations.

Keep root template state request-scoped so dynamic placeholders stay consistent during chunked renders, add reserve() coverage for non-streamed pages, and add compile/test coverage for the new root render lifecycle.
This commit is contained in:
2026-04-16 20:54:03 +10:00
parent bb1008435f
commit 9f111ab8bd
7 changed files with 239 additions and 14 deletions
+28
View File
@@ -0,0 +1,28 @@
name: Compile
on:
push:
pull_request:
jobs:
pio-compile:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
environment: [esp8266, esp32]
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install PlatformIO
run: pip install platformio
- name: Compile tests for ${{ matrix.environment }}
run: pio test -e ${{ matrix.environment }} --without-uploading --without-testing
@@ -76,6 +76,7 @@ class WiFiManagerServer {
void registerDefaultMenu(PlaceholderRegistry& reg);
void registerDefaultStatus(PlaceholderRegistry& reg);
void registerDefaultPlaceholders(PlaceholderRegistry& reg);
void applyTemplateSetupCallback(PlaceholderRegistry& reg);
// Rebuild registry with defaults and optional customizer
void rebuildPlaceholderRegistry(std::function<void(PlaceholderRegistry&)> customizer = nullptr);
+133 -9
View File
@@ -15,15 +15,94 @@
#include "templates/JS.h"
#include "templates/RootSelector.h"
#include <TemplateEngine.h>
#include <cstring>
#if defined(ESP8266) || defined(ESP32)
#ifndef WM_PAGE_RESERVE_BYTES
#define WM_PAGE_RESERVE_BYTES 8192
#endif
#ifndef WM_ROOT_REQUEST_STATE
#define WM_ROOT_REQUEST_STATE 1
#endif
namespace {
inline void reservePage(String& page, size_t extraBytes = WM_PAGE_RESERVE_BYTES) {
if (extraBytes == 0) return;
const size_t targetLen = page.length() + extraBytes;
(void)page.reserve(targetLen);
}
struct RootState {
String menu;
String status;
};
const char* rootMenuGetter(void* userData) {
const auto* state = static_cast<const RootState*>(userData);
return state ? state->menu.c_str() : "";
}
const char* rootStatusGetter(void* userData) {
const auto* state = static_cast<const RootState*>(userData);
return state ? state->status.c_str() : "";
}
size_t rootTemplateLengthGetter(const char* data, void* /*userData*/) {
return data ? strlen(data) : 0;
}
void escapePercentsForTemplate(String& value) {
if (value.indexOf('%') < 0) return;
String escaped;
reservePage(escaped, value.length() + 32);
for (size_t i = 0; i < value.length(); i++) {
if (value[i] == '%') {
escaped += F("&#37;");
} else {
escaped += value[i];
}
}
value = escaped;
}
void buildRootState(WiFiManagerHandlers* handlers, RootState& state) {
reservePage(state.menu, 768);
reservePage(state.status, 512);
handlers->getMenuOut(&state.menu);
handlers->reportStatus(state.status);
// Dynamic templates are parsed as template content, so escape literal '%' values.
escapePercentsForTemplate(state.menu);
escapePercentsForTemplate(state.status);
}
#if WM_ROOT_REQUEST_STATE
struct RootRenderBundle {
RootState state;
PlaceholderRegistry registry;
TemplateContext context;
DynamicTemplateDescriptor menuDescriptor;
DynamicTemplateDescriptor statusDescriptor;
RootRenderBundle()
: registry(12), menuDescriptor{}, statusDescriptor{} {}
};
#endif
} // namespace
WiFiManagerHandlers::WiFiManagerHandlers(WiFiManager* wm) : _wm(wm) {}
// Rendering Methods
String WiFiManagerHandlers::getHTTPHead(String title, String classes){
String page;
reservePage(page, 512);
page += FPSTR(HTML_HEAD_START);
page += title;
page += FPSTR(HTML_TITLE_END);
@@ -68,10 +147,11 @@ String WiFiManagerHandlers::getMenuOut(String* outOpt){
String WiFiManagerHandlers::getScanItemOut(){
String page;
int n = _wm->_numNetworks;
reservePage(page, 256 + (n > 0 ? static_cast<size_t>(n) * 96 : 96));
// Never trigger scans from here - only use cached data
// If no cached data, show message (scan should be started elsewhere if needed)
int n = _wm->_numNetworks;
if (n == 0) {
#ifdef WM_DEBUG_LEVEL
_wm->DEBUG_WM(F("No networks found"));
@@ -206,6 +286,7 @@ String WiFiManagerHandlers::getIpForm(String id, String title, String value){
String WiFiManagerHandlers::getStaticOut(){
String page;
reservePage(page, 384);
if ((_wm->_staShowStaticFields || _wm->_sta_static_ip) && _wm->_staShowStaticFields>=0) {
#ifdef WM_DEBUG_LEVEL
_wm->DEBUG_WM(WM_DEBUG_DEV,F("_staShowStaticFields"));
@@ -227,6 +308,7 @@ String WiFiManagerHandlers::getStaticOut(){
String WiFiManagerHandlers::getParamOut(){
String page;
reservePage(page, 256 + (_wm->_paramsCount > 0 ? static_cast<size_t>(_wm->_paramsCount) * 128 : 64));
#ifdef WM_DEBUG_LEVEL
_wm->DEBUG_WM(WM_DEBUG_DEV,F("getParamOut"),_wm->_paramsCount);
@@ -699,24 +781,52 @@ void WiFiManagerHandlers::handleRoot(AsyncWebServerRequest *request) {
#endif
if (captivePortal(request)) return;
handleRequest(request);
// Stream-render the root page using DFTE and the shared registry
AsyncWebServerResponse *response = nullptr;
#if WM_ROOT_REQUEST_STATE
auto bundle = std::make_shared<RootRenderBundle>();
buildRootState(this, bundle->state);
if (_wm->_serverManager) {
_wm->_serverManager->registerDefaultPlaceholders(bundle->registry);
_wm->_serverManager->applyTemplateSetupCallback(bundle->registry);
}
bundle->menuDescriptor.getter = &rootMenuGetter;
bundle->menuDescriptor.getLength = &rootTemplateLengthGetter;
bundle->menuDescriptor.userData = &bundle->state;
bundle->statusDescriptor.getter = &rootStatusGetter;
bundle->statusDescriptor.getLength = &rootTemplateLengthGetter;
bundle->statusDescriptor.userData = &bundle->state;
bundle->registry.registerDynamicTemplate("%MENU%", &bundle->menuDescriptor);
bundle->registry.registerDynamicTemplate("%STATUS%", &bundle->statusDescriptor);
bundle->context.setRegistry(&bundle->registry);
TemplateRenderer::initializeContext(bundle->context, WM_ROOT_TEMPLATE);
response = request->beginChunkedResponse(String(FPSTR(HTTP_HEAD_CT)),
[bundle](uint8_t *buffer, size_t maxLen, size_t /*index*/) -> size_t {
return TemplateRenderer::renderNextChunk(bundle->context, buffer, maxLen);
}
);
#else
TemplateContext ctx;
if (_wm->_serverManager && _wm->_serverManager->getPlaceholderRegistry()) {
ctx.setRegistry(_wm->_serverManager->getPlaceholderRegistry());
}
TemplateRenderer::initializeContext(ctx, WM_ROOT_TEMPLATE);
// Keep context alive across chunk calls
auto ctxPtr = std::make_shared<TemplateContext>(ctx);
AsyncWebServerResponse *response = request->beginChunkedResponse(String(FPSTR(HTTP_HEAD_CT)),
response = request->beginChunkedResponse(String(FPSTR(HTTP_HEAD_CT)),
[ctxPtr](uint8_t *buffer, size_t maxLen, size_t /*index*/) -> size_t {
if (!ctxPtr) return 0;
size_t written = TemplateRenderer::renderNextChunk(*ctxPtr, buffer, maxLen);
return written;
return TemplateRenderer::renderNextChunk(*ctxPtr, buffer, maxLen);
}
);
#endif
request->send(response);
if(_wm->_preloadwifiscan) _wm->WiFi_scanNetworks(_wm->_scancachetime);
}
@@ -734,6 +844,7 @@ void WiFiManagerHandlers::handleWifi(AsyncWebServerRequest *request, boolean sca
}
handleRequest(request);
String page = getHTTPHead(F("Config ESP"), FPSTR(C_wifi));
reservePage(page);
if (scan) {
bool forceRefresh = false;
if (request->hasParam("refresh")) {
@@ -805,6 +916,7 @@ void WiFiManagerHandlers::handleParam(AsyncWebServerRequest *request){
#endif
handleRequest(request);
String page = getHTTPHead(F("Setup"), FPSTR(C_param));
reservePage(page, 4096);
// Build form start directly without tokens
page += F("<form method='POST' action='paramsave'>");
@@ -901,10 +1013,12 @@ void WiFiManagerHandlers::handleWifiSave(AsyncWebServerRequest *request) {
if(_wm->_ssid == ""){
page = getHTTPHead(F("Settings saved"), FPSTR(C_wifi));
reservePage(page, 1024);
page += FPSTR(HTML_PARAMSAVED);
}
else {
page = getHTTPHead(F("Credentials saved"), FPSTR(C_wifi));
reservePage(page, 1024);
page += FPSTR(HTML_SAVED);
}
@@ -934,6 +1048,7 @@ void WiFiManagerHandlers::handleParamSave(AsyncWebServerRequest *request) {
doParamSave(requestArgs);
String page = getHTTPHead(F("Setup saved"), FPSTR(C_param));
reservePage(page, 1024);
page += FPSTR(HTML_PARAMSAVED);
if(_wm->_showBack) page += FPSTR(HTML_BACKBTN);
page += getHTTPEnd();
@@ -1005,6 +1120,7 @@ void WiFiManagerHandlers::handleInfo(AsyncWebServerRequest *request) {
#endif
handleRequest(request);
String page = getHTTPHead(F("Info"), FPSTR(C_info));
reservePage(page);
reportStatus(page);
uint16_t infos = 0;
@@ -1109,6 +1225,7 @@ void WiFiManagerHandlers::handleExit(AsyncWebServerRequest *request) {
#endif
handleRequest(request);
String page = getHTTPHead(F("Exit"), FPSTR(C_exit));
reservePage(page, 1024);
page += F("Exiting");
page += getHTTPEnd();
AsyncWebServerResponse *response = request->beginResponse(200, FPSTR(HTTP_HEAD_CT), page);
@@ -1125,6 +1242,7 @@ void WiFiManagerHandlers::handleReset(AsyncWebServerRequest *request) {
#endif
handleRequest(request);
String page = getHTTPHead(F("Reset"), FPSTR(C_restart));
reservePage(page, 1024);
page += F("Module will reset in a few seconds.");
page += getHTTPEnd();
@@ -1143,6 +1261,7 @@ void WiFiManagerHandlers::handleErase(AsyncWebServerRequest *request, boolean op
#endif
handleRequest(request);
String page = getHTTPHead(F("Erase"), FPSTR(C_erase));
reservePage(page, 1024);
bool ret = _wm->erase(opt);
@@ -1174,6 +1293,7 @@ void WiFiManagerHandlers::handleClose(AsyncWebServerRequest *request){
#endif
handleRequest(request);
String page = getHTTPHead(F("Close"), FPSTR(C_close));
reservePage(page, 1024);
page += F("You can close the page, portal will continue to run");
page += getHTTPEnd();
request->send(200, FPSTR(HTTP_HEAD_CT), page);
@@ -1196,6 +1316,7 @@ void WiFiManagerHandlers::handleWiFiStatus(AsyncWebServerRequest *request){
#endif
handleRequest(request);
String page;
reservePage(page, 256);
#ifdef WM_JSTEST
page = FPSTR(HTML_JS);
#endif
@@ -1209,6 +1330,7 @@ void WiFiManagerHandlers::handleWiFiScanStatus(AsyncWebServerRequest *request){
handleRequest(request);
String json = "{";
reservePage(json, 128 + (_wm->_numNetworks > 0 ? static_cast<size_t>(_wm->_numNetworks) * 96 : 64));
json += "\"scanning\":";
json += _wm->_scanInProgress ? "true" : "false";
json += ",\"count\":";
@@ -1275,6 +1397,7 @@ void WiFiManagerHandlers::handleUpdate(AsyncWebServerRequest *request) {
#endif
if (captivePortal(request)) return;
String page = getHTTPHead(_wm->_title, FPSTR(C_update));
reservePage(page, 2048);
// Build root main HTML directly without tokens
page += F("<h1>");
page += _wm->_title;
@@ -1360,6 +1483,7 @@ void WiFiManagerHandlers::handleUpdateDone(AsyncWebServerRequest *request) {
#endif
String page = getHTTPHead(F("options"), FPSTR(C_update));
reservePage(page, 2048);
// Build root main HTML directly without tokens
page += F("<h1>");
page += _wm->_title;
+9 -5
View File
@@ -99,13 +99,19 @@ void WiFiManagerServer::registerDefaultPlaceholders(PlaceholderRegistry& reg) {
registerDefaultStatus(reg);
}
void WiFiManagerServer::applyTemplateSetupCallback(PlaceholderRegistry& reg) {
if (_tplSetupCallback) {
_tplSetupCallback(reg);
}
}
void WiFiManagerServer::rebuildPlaceholderRegistry(std::function<void(PlaceholderRegistry&)> customizer) {
_tplRegistry = std::unique_ptr<PlaceholderRegistry>(new PlaceholderRegistry(12));
registerDefaultPlaceholders(*_tplRegistry);
if (customizer) {
customizer(*_tplRegistry);
} else if (_tplSetupCallback) {
_tplSetupCallback(*_tplRegistry);
} else {
applyTemplateSetupCallback(*_tplRegistry);
}
}
@@ -121,9 +127,7 @@ void WiFiManagerServer::setupTemplateEngine() {
registerDefaultPlaceholders(*_tplRegistry);
// Allow consumers to customize placeholders
if (_tplSetupCallback) {
_tplSetupCallback(*_tplRegistry);
}
applyTemplateSetupCallback(*_tplRegistry);
}
void WiFiManagerServer::createServer(uint16_t port) {
+2
View File
@@ -30,6 +30,8 @@ TestCase tests[] = {
TEST_ENTRY(test_config_portal_multiple_start_stop),
TEST_ENTRY(test_config_portal_already_active),
TEST_ENTRY(test_get_config_portal_ssid),
TEST_ENTRY(test_root_render_menu_state_transitions),
TEST_ENTRY(test_root_render_snapshot_consistency),
// Non-blocking tests
TEST_ENTRY(test_nonblocking_process),
+2
View File
@@ -40,6 +40,8 @@ void test_stop_web_portal();
void test_config_portal_multiple_start_stop();
void test_config_portal_already_active();
void test_get_config_portal_ssid();
void test_root_render_menu_state_transitions();
void test_root_render_snapshot_consistency();
// Non-blocking tests
void test_nonblocking_process();
@@ -0,0 +1,64 @@
#include <unity.h>
#include <Arduino.h>
#include <WiFiManager.h>
#include <WiFiManagerHandlers.h>
void test_root_render_menu_state_transitions() {
Serial.println("[TEST] Testing root menu state transitions...");
WiFiManager wm;
WiFiManagerHandlers handlers(&wm);
String menuBefore;
handlers.getMenuOut(&menuBefore);
TEST_ASSERT_EQUAL(-1, menuBefore.indexOf("/close"));
wm.setConfigPortalTimeout(5);
wm.startConfigPortal("RootStateAP");
wm.process();
delay(100);
String menuActive;
handlers.getMenuOut(&menuActive);
TEST_ASSERT_NOT_EQUAL(-1, menuActive.indexOf("/close"));
wm.stopConfigPortal();
delay(100);
String menuAfter;
handlers.getMenuOut(&menuAfter);
TEST_ASSERT_EQUAL(-1, menuAfter.indexOf("/close"));
Serial.println("[TEST] Root menu state transitions test completed successfully");
}
void test_root_render_snapshot_consistency() {
Serial.println("[TEST] Testing root render snapshot consistency...");
WiFiManager wm;
WiFiManagerHandlers handlers(&wm);
wm.setConfigPortalTimeout(5);
wm.startConfigPortal("RootSnapshotAP");
wm.process();
delay(100);
String menuSnapshot1;
String statusSnapshot1;
handlers.getMenuOut(&menuSnapshot1);
handlers.reportStatus(statusSnapshot1);
String menuSnapshot2;
String statusSnapshot2;
handlers.getMenuOut(&menuSnapshot2);
handlers.reportStatus(statusSnapshot2);
TEST_ASSERT_GREATER_THAN(0, menuSnapshot1.length());
TEST_ASSERT_GREATER_THAN(0, statusSnapshot1.length());
TEST_ASSERT_EQUAL_STRING(menuSnapshot1.c_str(), menuSnapshot2.c_str());
TEST_ASSERT_EQUAL_STRING(statusSnapshot1.c_str(), statusSnapshot2.c_str());
wm.stopConfigPortal();
Serial.println("[TEST] Root render snapshot consistency test completed successfully");
}