Improve heap management in EPUB rendering
Fix CPU pinning by tracking buildHeapPaused state when background build is blocked. Re-check heap gate after acquiring lock to handle race conditions where rendering expands glyph buffers. Refine plane buffer allocation to check both free heap and max contiguous block, preventing fragmentation issues that could cause OOM during page renders. Skip async refresh for image pages since they use blocking double-FAST path.
This commit is contained in:
@@ -260,12 +260,13 @@ void EpubReaderActivity::openReaderMenu() {
|
|||||||
bool EpubReaderActivity::buildTickHeapGate() {
|
bool EpubReaderActivity::buildTickHeapGate() {
|
||||||
const size_t freeHeap = ESP.getFreeHeap();
|
const size_t freeHeap = ESP.getFreeHeap();
|
||||||
const size_t maxBlock = ESP.getMaxAllocHeap();
|
const size_t maxBlock = ESP.getMaxAllocHeap();
|
||||||
if (freeHeap >= BACKGROUND_BUILD_MIN_FREE_HEAP && maxBlock >= BACKGROUND_BUILD_MIN_MAX_ALLOC) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
// Below the floors: just wait. The tick is deferrable — page-turn transients
|
// Below the floors: just wait. The tick is deferrable — page-turn transients
|
||||||
// free up between turns and the tick retries every loop pass.
|
// free up between turns and the tick retries every loop pass. Track the
|
||||||
return false;
|
// paused state so skipLoopDelay() stops pinning the CPU at full speed while
|
||||||
|
// no build work is actually happening (the gate can stay closed for a long
|
||||||
|
// stretch if the retained build context itself holds the heap down).
|
||||||
|
buildHeapPaused = freeHeap < BACKGROUND_BUILD_MIN_FREE_HEAP || maxBlock < BACKGROUND_BUILD_MIN_MAX_ALLOC;
|
||||||
|
return !buildHeapPaused;
|
||||||
}
|
}
|
||||||
|
|
||||||
void EpubReaderActivity::loop() {
|
void EpubReaderActivity::loop() {
|
||||||
@@ -349,10 +350,12 @@ void EpubReaderActivity::loop() {
|
|||||||
RenderLock lock;
|
RenderLock lock;
|
||||||
// Re-check under the lock: render() (which also holds the RenderLock) may have finalized the
|
// 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
|
// build between the outer isBuilding() check and acquiring the lock here, in which case
|
||||||
// buildSomeMore() would fail and wrongly reset the section. cppcheck can't see the cross-task
|
// buildSomeMore() would fail and wrongly reset the section. The heap gate must be re-read
|
||||||
// mutation, so it flags this as always true.
|
// too: a render that won the lock race can expand retained glyph buffers, invalidating the
|
||||||
|
// pre-lock heap reading. cppcheck can't see the cross-task mutation, so it flags this as
|
||||||
|
// always true.
|
||||||
// cppcheck-suppress knownConditionTrueFalse
|
// cppcheck-suppress knownConditionTrueFalse
|
||||||
if (section->isBuilding()) {
|
if (section->isBuilding() && buildTickHeapGate()) {
|
||||||
if (!section->buildSomeMore(BACKGROUND_BUILD_PAGES_PER_TICK)) {
|
if (!section->buildSomeMore(BACKGROUND_BUILD_PAGES_PER_TICK)) {
|
||||||
LOG_ERR("ERS", "Background section build failed");
|
LOG_ERR("ERS", "Background section build failed");
|
||||||
section.reset();
|
section.reset();
|
||||||
@@ -1401,8 +1404,10 @@ void EpubReaderActivity::renderContents(std::unique_ptr<Page> page, const int or
|
|||||||
const bool tiledGrayscale = needsAnyGrayscale && renderer.supportsStripGrayscale();
|
const bool tiledGrayscale = needsAnyGrayscale && renderer.supportsStripGrayscale();
|
||||||
// Whole-plane buffering only pays when the BW refresh genuinely runs async
|
// Whole-plane buffering only pays when the BW refresh genuinely runs async
|
||||||
// underneath it; on blocking panels (X3) it would just spend ~50 KB for the
|
// underneath it; on blocking panels (X3) it would just spend ~50 KB for the
|
||||||
// identical serial timing.
|
// identical serial timing. Image pages take the blocking double-FAST path
|
||||||
const bool overlapRefresh = tiledGrayscale && renderer.supportsAsyncRefresh();
|
// below (no async refresh is ever started), so they'd spend the buffers with
|
||||||
|
// nothing in flight to overlap.
|
||||||
|
const bool overlapRefresh = tiledGrayscale && renderer.supportsAsyncRefresh() && !pageHasImages;
|
||||||
auto renderGrayscalePass = [&]() {
|
auto renderGrayscalePass = [&]() {
|
||||||
if (needsTextGrayscale) {
|
if (needsTextGrayscale) {
|
||||||
page->render(renderer, fontId, orientedMarginLeft, orientedMarginTop);
|
page->render(renderer, fontId, orientedMarginLeft, orientedMarginTop);
|
||||||
@@ -1485,12 +1490,25 @@ void EpubReaderActivity::renderContents(std::unique_ptr<Page> page, const int or
|
|||||||
// Tiered on heap pressure: two plane buffers hide both plane renders
|
// Tiered on heap pressure: two plane buffers hide both plane renders
|
||||||
// inside the refresh wait; one hides the LSB render (its buffer is reused
|
// inside the refresh wait; one hides the LSB render (its buffer is reused
|
||||||
// for MSB after streaming); none falls back to the strip-scratch flow with
|
// for MSB after streaming); none falls back to the strip-scratch flow with
|
||||||
// no overlap. The MSB buffer is only attempted when it leaves ~60 KB free
|
// no overlap. Each buffer is only attempted when it leaves ~60 KB free so
|
||||||
// so the pass never starves concurrent allocations. Blocking panels skip
|
// the pass never starves concurrent allocations: the next page re-render
|
||||||
// the buffers entirely (nothing to overlap).
|
// allocates through throwing std::string paths that abort() on OOM under
|
||||||
auto lsbPlaneBuf = overlapRefresh ? makeUniqueNoThrow<uint8_t[]>(planeBytes) : nullptr;
|
// -fno-exceptions, so a plane buffer that "fits" but eats the render
|
||||||
auto msbPlaneBuf =
|
// headroom is worse than the strip fallback. Blocking panels skip the
|
||||||
(lsbPlaneBuf && ESP.getFreeHeap() >= planeBytes + 60000) ? makeUniqueNoThrow<uint8_t[]>(planeBytes) : nullptr;
|
// buffers entirely (nothing to overlap).
|
||||||
|
constexpr size_t PLANE_BUF_HEADROOM = 60000;
|
||||||
|
// Free-heap alone ignores fragmentation: taking the largest block for a
|
||||||
|
// plane can leave only slivers behind even when total headroom looks fine.
|
||||||
|
// Require the block to fit the plane with 16 KB contiguous to spare, which
|
||||||
|
// also keeps the advance-table batch scratch viable mid-render (same
|
||||||
|
// rationale as BACKGROUND_BUILD_MIN_MAX_ALLOC).
|
||||||
|
constexpr size_t PLANE_BUF_MAX_ALLOC_RESERVE = 16 * 1024;
|
||||||
|
const auto planeBufFits = [planeBytes] {
|
||||||
|
return ESP.getFreeHeap() >= planeBytes + PLANE_BUF_HEADROOM &&
|
||||||
|
ESP.getMaxAllocHeap() >= planeBytes + PLANE_BUF_MAX_ALLOC_RESERVE;
|
||||||
|
};
|
||||||
|
auto lsbPlaneBuf = (overlapRefresh && planeBufFits()) ? makeUniqueNoThrow<uint8_t[]>(planeBytes) : nullptr;
|
||||||
|
auto msbPlaneBuf = (lsbPlaneBuf && planeBufFits()) ? makeUniqueNoThrow<uint8_t[]>(planeBytes) : nullptr;
|
||||||
|
|
||||||
if (lsbPlaneBuf) {
|
if (lsbPlaneBuf) {
|
||||||
renderPlaneToBuffer(true, lsbPlaneBuf.get());
|
renderPlaneToBuffer(true, lsbPlaneBuf.get());
|
||||||
|
|||||||
@@ -109,7 +109,13 @@ class EpubReaderActivity final : public Activity {
|
|||||||
// batch path (16 KB scratch) viable during builds.
|
// batch path (16 KB scratch) viable during builds.
|
||||||
static constexpr size_t BACKGROUND_BUILD_MIN_MAX_ALLOC = 16 * 1024;
|
static constexpr size_t BACKGROUND_BUILD_MIN_MAX_ALLOC = 16 * 1024;
|
||||||
// Gate for a background build tick: true when the heap can take parse allocations.
|
// Gate for a background build tick: true when the heap can take parse allocations.
|
||||||
|
// Updates buildHeapPaused as a side effect.
|
||||||
bool buildTickHeapGate();
|
bool buildTickHeapGate();
|
||||||
|
// True while the background build is gated on the heap floors. Lets skipLoopDelay()
|
||||||
|
// return the loop to normal delay/power-saving during the pause: isBuilding() stays
|
||||||
|
// true the whole time, and without this the loop would spin at full CPU speed doing
|
||||||
|
// no build work — indefinitely, if the build context itself keeps the heap low.
|
||||||
|
bool buildHeapPaused = false;
|
||||||
// Heap floor for optional render-adjacent work (idle prewarm). Page
|
// Heap floor for optional render-adjacent work (idle prewarm). Page
|
||||||
// deserialization (TextBlock word vectors/strings) and glyph caching allocate
|
// deserialization (TextBlock word vectors/strings) and glyph caching allocate
|
||||||
// through throwing paths that abort() on OOM; skip deferrable work below it.
|
// through throwing paths that abort() on OOM; skip deferrable work below it.
|
||||||
@@ -171,8 +177,10 @@ class EpubReaderActivity final : public Activity {
|
|||||||
// Full CPU speed + fast loop ticks while a section build runs: at the low-power
|
// Full CPU speed + fast loop ticks while a section build runs: at the low-power
|
||||||
// frequency a giant chapter's background rebuild stretches from ~40s to many
|
// frequency a giant chapter's background rebuild stretches from ~40s to many
|
||||||
// minutes, so the reader exits before it can finalize and the next open restarts
|
// minutes, so the reader exits before it can finalize and the next open restarts
|
||||||
// it from page 0. Reverts to normal power behavior the moment the build finishes.
|
// it from page 0. Reverts to normal power behavior the moment the build finishes,
|
||||||
bool skipLoopDelay() override { return section && section->isBuilding(); }
|
// and while the build is heap-paused (no work is happening, so spinning at full
|
||||||
|
// speed would only burn battery; the paused gate still retries every loop pass).
|
||||||
|
bool skipLoopDelay() override { return section && section->isBuilding() && !buildHeapPaused; }
|
||||||
bool isReaderActivity() const override { return true; }
|
bool isReaderActivity() const override { return true; }
|
||||||
ScreenshotInfo getScreenshotInfo() const override;
|
ScreenshotInfo getScreenshotInfo() const override;
|
||||||
CrossPointPosition getCurrentPosition() const;
|
CrossPointPosition getCurrentPosition() const;
|
||||||
|
|||||||
Reference in New Issue
Block a user