Add heap pressure management for BLE and rendering

Prevent OOM aborts by shedding BLE stack when free heap drops below 24KB before rendering. Lower BLE start threshold from 100KB to 80KB to match steady-state heap levels (~84KB). Reserve vector capacity upfront in page parsing to avoid reallocation crashes on fragmented heaps. Gate background section builds on heap availability.
This commit is contained in:
Justin Mitchell
2026-07-06 08:12:11 -04:00
parent 0d7a84e3c5
commit 613371b716
6 changed files with 83 additions and 10 deletions
+19 -2
View File
@@ -269,9 +269,12 @@ void EpubReaderActivity::loop() {
// RenderLock and locked out page turns. The build follows the reader instead, and instant
// reopen comes from suspendBuild() persisting the laid-out pages as a partial on exit.
// Skip while the render mutex is busy so we never delay a pending render; re-check
// isBuilding() under the lock since render() may have just finished it.
// isBuilding() under the lock since render() may have just finished it. Also skip
// while free heap is below the floor — the tick is deferrable, and parsing into a
// starved heap abort()s (see BACKGROUND_BUILD_MIN_FREE_HEAP).
if (section && section->isBuilding() && !RenderLock::peek() &&
static_cast<int>(section->pageCount) < section->currentPage + BUILD_WINDOW_AHEAD) {
static_cast<int>(section->pageCount) < section->currentPage + BUILD_WINDOW_AHEAD &&
ESP.getFreeHeap() >= BACKGROUND_BUILD_MIN_FREE_HEAP) {
RenderLock lock;
// Re-check under the lock: render() (which also holds the RenderLock) may have finalized the
// build between the outer isBuilding() check and acquiring the lock here, in which case
@@ -898,6 +901,20 @@ void EpubReaderActivity::render(RenderLock&& lock) {
~LifecycleUnpause() { bleinput::setLifecyclePaused(false); }
} lifecycleUnpause;
// Shed the BLE stack before rendering into a starved heap. Everything below —
// page deserialization, glyph caching, catch-up build steps — allocates through
// throwing paths that abort() on OOM under -fno-exceptions. Field data: with no
// shed, a session ground to <2.2 KB free (per-glyph SD fallbacks, no AA, 4.7 s
// pages) and then aborted on a tiny vector growth. Freeing BLE returns ~52 KB and
// restores a large contiguous block; the lifecycle restarts it behind its heap
// gate once the pressure passes.
if (BleHid.isRunning() && ESP.getFreeHeap() < RENDER_MIN_FREE_HEAP) {
LOG_ERR("ERS", "Render heap %u below floor %u; freeing BLE RAM", (unsigned)ESP.getFreeHeap(),
(unsigned)RENDER_MIN_FREE_HEAP);
bleinput::setLifecyclePaused(true);
bleinput::stop();
}
const auto showPendingSyncSaveError = [this]() {
if (!pendingSyncSaveError) return;
pendingSyncSaveError = false;
@@ -67,6 +67,13 @@ class EpubReaderActivity final : public Activity {
// (MIN_FREE_HEAP_FOR_CSS), so 40 KB trades a few early BLE teardowns for not crashing.
static constexpr size_t BUILD_MIN_FREE_HEAP = 40 * 1024;
// Heap floor for rendering a page at all. Page deserialization (TextBlock word
// vectors/strings) and glyph caching allocate through throwing paths that abort()
// on OOM; below this floor render() sheds the BLE stack (~52 KB back, and it
// restores a large contiguous block) before touching the page. Field data: a
// session with no shed ground to <2.2 KB free and aborted on a page load.
static constexpr size_t RENDER_MIN_FREE_HEAP = 24 * 1024;
void renderContents(std::unique_ptr<Page> page, int orientedMarginTop, int orientedMarginRight,
int orientedMarginBottom, int orientedMarginLeft);
void renderStatusBar() const;
@@ -75,6 +82,14 @@ class EpubReaderActivity final : public Activity {
// background build chunk never noticeably delays input or a pending render.
static constexpr int BUILD_PAGES_PER_CHUNK = 8;
static constexpr int BACKGROUND_BUILD_PAGES_PER_TICK = 2;
// Skip background build ticks below this free-heap floor. The parse path grows
// word vectors of heap strings — throwing allocations that abort() on OOM under
// -fno-exceptions (field crash: bad_alloc in ParsedText::addWord during a
// background tick with the BLE stack resident). The tick is deferrable work:
// page-turn transients free up between turns and the build resumes; the render
// path still builds the page it actually needs regardless of this floor.
static constexpr size_t BACKGROUND_BUILD_MIN_FREE_HEAP = 32 * 1024;
// How many pages to keep laid out ahead of the reader for a still-building section. A page
// turn is ~1s on e-ink and a page builds in ~30ms, so the reader can't out-click the builder
// -- a tiny buffer is enough. The background build stops once the watermark is this far