Repository navigation
zephyr-cp: let the VM heap grow into Zephyr's malloc arena - #11687
Merged
Merged
Conversation
port_heap_get_largest_free_size() now counts the free bytes in Zephyr's malloc arena (CONFIG_SYS_HEAP_RUNTIME_STATS), minus alignment padding and CONFIG_CIRCUITPY_ZEPHYR_MALLOC_RESERVE (default 4096). The reserve is left for Zephyr's own allocations, such as for TLS and BLE pairing. Previously single-region boards reported a fixed 64 KB. gc_try_add_heap() retries a failed area allocation with halved sizes down to the size the allocation needs, because the free-byte count can exceed the largest free block in a fragmented arena. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Code and measurements by Claude, reviewed and edited by @dhalbert.
Improve free RAM utilization for the GC heap on zephyr-cp, and allow larger objects.
Problems
Some zephyr-cp boards assign the entire amount of unused RAM to Zephyr's malloc arena. This includes nordic and RP2. On these boards,
port_heap_get_largest_free_size()always returned 64 kB. The CircuitPython VM heap grows by adding areas from the arena, and it uses that number as the size limit for a new area. So on these boards:A more minor problem was that
gc.mem_free()was inaccurate on those zephyr-cp boards. It adds the free space in the existing GC areas to the value ofport_heap_get_largest_free_size(). Because that value was a fixed 64 kB,gc.mem_free()never read below 64 kB, even when no free RAM was available.Changes
port_heap_get_largest_free_size():CONFIG_SYS_HEAP_RUNTIME_STATSoption.CONFIG_CIRCUITPY_ZEPHYR_MALLOC_RESERVE. It's 4096 by default, and a board can change it in itsboard.conf. This is enough for BLE. TLS needs much more, about 37 kB per connection, so it can still fail when the heap is nearly full. We might adjust this in the future.gc_try_add_heap()inpy/gc.casks for a new area as large as the whole heap so far, capped byport_heap_get_largest_free_size(). Previously, if that allocation failed, it gave up, even when a smaller area would have fit. Now it retries, halving the size each time, down to the size the failed allocation needs plus area overhead.Note that the info enabled by
CONFIG_SYS_HEAP_RUNTIME_STATSonly reports total free bytes, which could be the sum of fragmented regions, not the arena's largest free block. So when it is fragmented the first request could be too large, and it will try again. On TLSF ports like espressif, where the size limit is an exact free-block size, the first request should not fail and the retry loop does not run.gc.mem_free()is now more accurate on zephyr-cp boards, with no change to its code, becauseport_heap_get_largest_free_size()now reports the arena's actual free space. The value is still somewhat approximate because:Testing
MemoryError, then added a BLE service, advertised, and paired from a Linux host.MemoryError, no crashgc.mem_free()went from about 285 kB at the start to about 6 kB with the heap full.make teston native_sim passed.A regression test was also done on the classic
raspberrypiport, Pico 2 W, since thepy/gc.cchange applies to all ports. The same TLS test worked as above, and TLS also worked while holding 130 to 255 kB of Python data.Later: gentler heap growth
gc_try_add_heap()makes each new area as large as the whole heap so far, capped byport_heap_get_largest_free_size(). So the area sizes double, and near the end of RAM one growth step can take most of what's left in the arena, even when Python doesn't need that much. Then TLS can fail even though the GC heap has plenty of free space.A fix, not in this PR, is to cap each new area at half of what's left in the arena, unless the failed allocation needs more. Early growth would be unchanged, but near the end the arena's free space would shrink gradually instead of all at once.
Later: more detailed heap reporting
I have an idea for a more comprehensive
supervisor.mem_info()that would report on all the GC heap pools, giving the free space and largest available space in each one, and how much more free RAM is still available for pools.Alternative: one TLSF heap for everything
An alternative approach, not done here, is to replace Zephyr's
malloc()with CircuitPython's TLSF allocator, so that the VM heap and Zephyr's own allocations share one heap across all RAM regions. That is similar to how espressif works: ESP-IDF and CircuitPython already share one TLSF-based heap. This approach would:zephyr,sram, like the i.MX RT1011 boards, once they move to zephyr-cp.This PR doesn't do that, because replacing libc
malloc()is a larger and riskier change. Every Zephyr allocation would go through it, and it would have to be set up beforemain()and be thread-safe. It could be considered in the future, for example if the arena on WiFi boards fragments enough to cause earlyMemoryErrors.