Skip to content

fix(mem): lower LVGL hybrid PSRAM threshold 1024->256 (heap-starvation reboots)#39

Open
torlando-tech wants to merge 1 commit into
mainfrom
fix/heap-psram-threshold
Open

fix(mem): lower LVGL hybrid PSRAM threshold 1024->256 (heap-starvation reboots)#39
torlando-tech wants to merge 1 commit into
mainfrom
fix/heap-psram-threshold

Conversation

@torlando-tech

Copy link
Copy Markdown
Owner

Lowers the LVGL hybrid-allocator PSRAM threshold 1024→256, moving ~45 KB of small LVGL allocations (styles, obj metadata, labels, anim descriptors) into PSRAM. This de-fragments the scarce internal heap (internal free 71→116 KB, largest contiguous block 61→106 KB) so the LXST call/loopback audio pipeline can allocate reliably during a call instead of intermittently failing and rebooting mid-call.

System-wide allocator tuning; no audio/clock coupling. Found during the LXST voice investigation.

🤖 Generated with Claude Code

https://claude.ai/code/session_01UWZuYkHBRqNb6BZHV8sTG5

…n reboots)

Moves ~45KB of small LVGL allocations to PSRAM (internal free 71->116KB, largest contiguous 61->106KB), fixing the internal-heap starvation that made the LXST call/loopback pipeline intermittently fail to allocate and reboot mid-call. System-wide allocator tuning; no audio/clock coupling.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Claude-Session: https://claude.ai/code/session_01UWZuYkHBRqNb6BZHV8sTG5
@greptile-apps

greptile-apps Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR lowers the LVGL hybrid-allocator PSRAM threshold from 1024 to 256 bytes, routing more small LVGL objects (styles, labels, animation descriptors) into PSRAM to relieve internal-heap fragmentation and prevent mid-call reboots in the LXST audio pipeline.

  • The threshold change is well-motivated and the comment documents the rationale clearly; the measured improvement (internal free 71→116 KB, largest block 61→106 KB) is plausible for this kind of workload.
  • A pre-existing but latent bug in lv_mem_hybrid_realloc is surfaced at much higher frequency by the lower threshold: when an internal-RAM allocation is realloced to a size ≥ 256 bytes, the code allocates new PSRAM, frees the old pointer, but never copies the old bytes — returning a pointer to uninitialized memory. At 1024 bytes this path was rarely hit; at 256 bytes it fires on routine LVGL label and array growth.

Confidence Score: 3/5

The threshold change achieves its stated goal of relieving internal-heap pressure, but it exposes a latent data-corruption bug in the realloc path that now fires frequently during normal LVGL operation.

The realloc migration path (internal → PSRAM) frees the old block without copying its contents, returning a pointer to uninitialized memory. At 1024 bytes this almost never triggered in practice; at 256 bytes it will fire on any LVGL label or array that grows through a realloc — corrupting UI state silently rather than crashing predictably. The threshold change is the right direction for the heap-fragmentation problem, but shipping it without fixing the copy omission trades one instability (OOM reboots) for another (random heap corruption).

lib/lv_mem_hybrid.h — the realloc cross-region migration paths at lines 68–74 and 78–86 both need a memcpy before the old pointer is freed.

Important Files Changed

Filename Overview
lib/lv_mem_hybrid.h Threshold lowered from 1024→256 bytes; exposes a pre-existing realloc bug where data is not copied when an allocation migrates from internal RAM to PSRAM, now triggered far more frequently by normal LVGL realloc patterns

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["lv_mem_hybrid_realloc(ptr, size)"] --> B{"ptr == NULL?"}
    B -- yes --> C["lv_mem_hybrid_alloc"]
    B -- no --> D{"size == 0?"}
    D -- yes --> E["free + return NULL"]
    D -- no --> F{"IS_PSRAM_ADDR"}
    F -- yes --> G["realloc in PSRAM"]
    G -- fail --> H["malloc internal then free old WITHOUT copy"]
    F -- no --> I{"size >= THRESHOLD now 256"}
    I -- yes --> J["malloc PSRAM then free old WITHOUT copy"]
    J -- fail --> K["realloc internal"]
    I -- no --> K
    K -- fail --> L["realloc PSRAM cross-region"]

    style J fill:#ff6666,color:#fff
    style H fill:#ff6666,color:#fff
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A["lv_mem_hybrid_realloc(ptr, size)"] --> B{"ptr == NULL?"}
    B -- yes --> C["lv_mem_hybrid_alloc"]
    B -- no --> D{"size == 0?"}
    D -- yes --> E["free + return NULL"]
    D -- no --> F{"IS_PSRAM_ADDR"}
    F -- yes --> G["realloc in PSRAM"]
    G -- fail --> H["malloc internal then free old WITHOUT copy"]
    F -- no --> I{"size >= THRESHOLD now 256"}
    I -- yes --> J["malloc PSRAM then free old WITHOUT copy"]
    J -- fail --> K["realloc internal"]
    I -- no --> K
    K -- fail --> L["realloc PSRAM cross-region"]

    style J fill:#ff6666,color:#fff
    style H fill:#ff6666,color:#fff
Loading

Comments Outside Diff (1)

  1. lib/lv_mem_hybrid.h, line 78-86 (link)

    P1 Silent data loss when realloc crosses the threshold upward

    lv_mem_hybrid_realloc frees the old internal-RAM pointer (line 82) after allocating new PSRAM, but never copies the old contents. The returned new_ptr holds uninitialized bytes. This corruption was harder to reach at threshold=1024 because a realloc would need to cross that 1 KB boundary; at threshold=256 it fires on any LVGL buffer that grows past 256 bytes (label text, flex-child arrays, style lists, etc.), making silent heap corruption far more likely during normal UI operation.

    The comment on line 70 in the PSRAM→internal fallback path also says "Copy and free old" but the copy is never performed — same root bug, different branch.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: lib/lv_mem_hybrid.h
    Line: 78-86
    
    Comment:
    **Silent data loss when realloc crosses the threshold upward**
    
    `lv_mem_hybrid_realloc` frees the old internal-RAM pointer (line 82) after allocating new PSRAM, but **never copies the old contents**. The returned `new_ptr` holds uninitialized bytes. This corruption was harder to reach at threshold=1024 because a realloc would need to cross that 1 KB boundary; at threshold=256 it fires on any LVGL buffer that grows past 256 bytes (label text, flex-child arrays, style lists, etc.), making silent heap corruption far more likely during normal UI operation.
    
    The comment on line 70 in the PSRAM→internal fallback path also says "Copy and free old" but the copy is never performed — same root bug, different branch.
    
    How can I resolve this? If you propose a fix, please make it concise.

    Fix in Claude Code

Fix All in Claude Code

Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 1
lib/lv_mem_hybrid.h:78-86
**Silent data loss when realloc crosses the threshold upward**

`lv_mem_hybrid_realloc` frees the old internal-RAM pointer (line 82) after allocating new PSRAM, but **never copies the old contents**. The returned `new_ptr` holds uninitialized bytes. This corruption was harder to reach at threshold=1024 because a realloc would need to cross that 1 KB boundary; at threshold=256 it fires on any LVGL buffer that grows past 256 bytes (label text, flex-child arrays, style lists, etc.), making silent heap corruption far more likely during normal UI operation.

The comment on line 70 in the PSRAM→internal fallback path also says "Copy and free old" but the copy is never performed — same root bug, different branch.

Reviews (1): Last reviewed commit: "fix(mem): lower LVGL hybrid PSRAM thresh..." | Re-trigger Greptile

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant