fix(layout): reserve growable tree vectors up front to avoid bad_alloc

Reserve nodes/specs/styles/labelAt/labels at reset() while the heap still
has a large contiguous block. A doubling realloc mid-build needs old+new
buffers live at once and throws bad_alloc on the heap the build itself has
fragmented; the styles vector was the one tripping the firmware's global
new-handler into a reset with ~10 populated cards.

UNVALIDATED: reproduced and fixed in the emulator only. Hardware has ~30KB
less free (NimBLE DMA buffers), so the reserves at reset() may still be
marginal there -- not yet tested on the board.
This commit is contained in:
2026-08-08 08:49:54 -04:00
parent a09bcfaf06
commit 873e003c45
+12 -1
View File
@@ -144,7 +144,10 @@ public:
// Growth doubling needs the old and new buffers live at once, which is the
// allocation that fails first on a tight heap. One reservation covers a
// typical screen so a build reallocates only if it genuinely gets large.
static constexpr size_t RESERVE = 64;
// 512 skips most growth-doubling reallocs during a build (a typical screen is
// ~300 nodes); 64 forced 4-5 doubling moves, each fragmenting the heap while
// old and new buffers are both live.
static constexpr size_t RESERVE = 512;
void reset() {
focus = NONE;
@@ -156,6 +159,14 @@ public:
labels.clear();
styles.clear();
scrollState.clear();
// Reserve every growable vector while the heap still has a large contiguous
// block. A doubling realloc mid-build needs old+new live at once and throws
// bad_alloc on a heap the build has already fragmented -- the styles vector
// is what the global new-handler caught and turned into a reset. styles is
// sparser than nodes; labels is a byte arena.
labelAt.reserve(RESERVE);
styles.reserve(RESERVE / 2);
labels.reserve(1024);
error = nullptr;
}