You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Obviously the buffer is necessary (for any streams you don't control the creation of, almost always w/ wbits=15), but that's a third of available ram, and sometimes the GC doesn't purge the old one in time to create the new one, or because of fragmentation there's no contiguous space available.
Could we add a function to reset the internal state? IE decomp.reset(f) That way it could reuse the previous allocation, eliminating the risk of an OOM if resetting instead of creating anew.
Would you be willing to sponsor this work? Yes, if the concept is agreed to.
A pointer to it is stored on o->decomp, so the DecompIO object will hold a reference to it, preventing it from being garbage collected until it goes out of scope or is explicitly del'd.
Since DecompIO has no hidden global, internal state then decomp.reset(f) couldn't do anything different to:
deldecompgc.collect()
I'm guessing the canonical answer would be to manually gc.collect(), though garbage collection should keep up. On the RP2 port with a bit more RAM (about 187k) I can run this indefinitely:
HOWEVER - for some reason, if I manually run uzlib.DecompIO(open("witw.z", "rb")) on the REPL via Thonny it never lets go of the buffer, even if I explicitly call gc.collect():
Normal program flow. I'm writing a git client for my esp32, which involves lots of zlib streams. (Code is already structured to only ever open one at a time, even for DELTA objs.)
del decomp; gc.collect() is basically what I'm doing, but it's non-deterministically failing. (My esp32 has about 100k available ram.) The malloc sometimes will fail even when mem_free reports over 32768. I believe because there's not a single contiguous chunk of 32768. I can do the first example indefinitely too.
I believe the malloc failure you experienced is due to that as well. (Not that REPL is holding a reference.) Try it without the print statements (or any other additional object creation). I can do that second example indefinitely without the prints, as it keeps freeing and reallocating the same chunk. But once you start creating random strings, memory gets fragmented, and occasionally it'll fail even when total available ram is enough.
__enter__ <_ObjReader object at 3ffe69c0> 3
before 68432 after 69936 @ <_ObjReader object at 3ffe69c0>
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "mgit.py", line 476, in clone
File "mgit.py", line 595, in pull
File "mgit.py", line 495, in checkout
File "mgit.py", line 526, in _checkout_file
File "mgit.py", line 211, in __enter__
MemoryError: memory allocation failed, allocating 32768 bytes
You can see it fails even with 69936 free reported.
Whereas other runs (>50% of the time) will happily run until the file-system fills up:
__enter__ <_ObjReader object at 3ffe6650> 3
before 70768 after 72224 @ <_ObjReader object at 3ffe6650>
__exit__ <_ObjReader object at 3ffe6650> 3 True
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "mgit.py", line 470, in clone
File "mgit.py", line 589, in pull
File "mgit.py", line 489, in checkout
File "mgit.py", line 523, in _checkout_file
OSError: 28
I guess self.decompressed_stream will hold a reference to DecompIO() that's not released until you're in C scope creating a new DecompIO object, so there's no opportunity for GC to run and collect that free'd RAM.
That will usually work, but would make for some pretty weird code outside a trivial example.
I don't think the underlying problem (lack of a free 32k block even when total free ram is >32k) is fixable. But a simple reset function would be useful and easy to implement. For example, rather than:
I'm definitely not qualified to yay or nay a PR, but I agree with the premise here.
Though I'm not sure MicroPython is necessarily averse to weird code 😆 - for better or worse, writing an interpreted language in an embedded environment is not always going to be graceful.
It feels like the fundamental goal here is to have a fixed region of RAM dedicated to zlib decompression, such that you're never at risk of fragmentation or allocation failure.
I'd guess the approach most likely to be accepted is to make the filename optional in the constructor, like so:
The above interface would prep the DecompIO object with a suitable buffer size (calculated from wbits) and calls to open_stream would raise a ValueError or otherwise for streams that need a larger window size. open_stream is effectively your reset.
That way your window size isn't a pot-luck from the first compressed stream you decompress, and you don't need to worry about potential re-allocations (or baffling the user) later.
there's some housekeeping needed. arg checks. what to do if the wbits are different between the two streams.
also i can't figure out how to make it return none... 🤦 but it works! (and no more OOMs when parsing a few hundred small streams!)
Activity
Gadgetoid commentedon Mar 28, 2023
Looks like that 32k buffer is allocated here:
micropython/extmod/moduzlib.c
Line 104 in 38e7b84
When it is passed to
uzlib_uncompress_init.A pointer to it is stored on
o->decomp, so theDecompIOobject will hold a reference to it, preventing it from being garbage collected until it goes out of scope or is explicitlydel'd.Since
DecompIOhas no hidden global, internal state thendecomp.reset(f)couldn't do anything different to:I'm guessing the canonical answer would be to manually
gc.collect(), though garbage collection should keep up. On the RP2 port with a bit more RAM (about 187k) I can run this indefinitely:HOWEVER - for some reason, if I manually run
uzlib.DecompIO(open("witw.z", "rb"))on the REPL via Thonny it never lets go of the buffer, even if I explicitly callgc.collect():This implies that the REPL is holding a reference to every
DecompIOobject somehow, and preventing the RAM from being freed.Is it normal program flow or REPL usage that you're encountering this issue with?
Here's my test file (renamed to .zip so GitHub will accept it):
witw.zip
keredson commentedon Mar 28, 2023
Normal program flow. I'm writing a
gitclient for my esp32, which involves lots of zlib streams. (Code is already structured to only ever open one at a time, even for DELTA objs.)del decomp; gc.collect()is basically what I'm doing, but it's non-deterministically failing. (My esp32 has about 100k available ram.) The malloc sometimes will fail even whenmem_freereports over 32768. I believe because there's not a single contiguous chunk of 32768. I can do the first example indefinitely too.I believe the malloc failure you experienced is due to that as well. (Not that REPL is holding a reference.) Try it without the print statements (or any other additional object creation). I can do that second example indefinitely without the prints, as it keeps freeing and reallocating the same chunk. But once you start creating random strings, memory gets fragmented, and occasionally it'll fail even when total available ram is enough.
keredson commentedon Mar 28, 2023
example:
code:
You can see it fails even with 69936 free reported.
Whereas other runs (>50% of the time) will happily run until the file-system fills up:
Gadgetoid commentedon Mar 28, 2023
I guess
self.decompressed_streamwill hold a reference toDecompIO()that's not released until you're in C scope creating a newDecompIOobject, so there's no opportunity for GC to run and collect that free'd RAM.Have you tried something like:
keredson commentedon Mar 28, 2023
That will usually work, but would make for some pretty weird code outside a trivial example.
I don't think the underlying problem (lack of a free 32k block even when total free ram is >32k) is fixable. But a simple reset function would be useful and easy to implement. For example, rather than:
just do:
This function should be simple enough:
uzlib_gzip_parse_header/etc.And doesn't require a risky 32k malloc.
I'll be happy to work on a PR if you're not opposed to it on principal.
Gadgetoid commentedon Mar 28, 2023
I'm definitely not qualified to yay or nay a PR, but I agree with the premise here.
Though I'm not sure MicroPython is necessarily averse to weird code 😆 - for better or worse, writing an interpreted language in an embedded environment is not always going to be graceful.
It feels like the fundamental goal here is to have a fixed region of RAM dedicated to zlib decompression, such that you're never at risk of fragmentation or allocation failure.
I'd guess the approach most likely to be accepted is to make the filename optional in the constructor, like so:
There's precedent for zlib libraries expecting the user to know about window size, since CPython's zlib has a
wbitsargument: https://docs.python.org/3/library/zlib.html#zlib.decompressThe above interface would prep the DecompIO object with a suitable buffer size (calculated from
wbits) and calls toopen_streamwould raise aValueErroror otherwise for streams that need a larger window size.open_streamis effectively yourreset.That way your window size isn't a pot-luck from the first compressed stream you decompress, and you don't need to worry about potential re-allocations (or baffling the user) later.
Gadgetoid commentedon Mar 28, 2023
Oh hey I totally missed that
DecompIOdoes acceptwbitsas a second positional argument as part ofdict_opt.micropython/extmod/moduzlib.c
Lines 84 to 104 in 38e7b84
Where
dict_optis rather awkwardly used to disambiguate gzip vs zlib or, with a negative number, just assumed to be for a raw DEFLATE stream. Ugh.The docs say the API is unstable, though, so fill your boots I guess! - https://docs.micropython.org/en/latest/library/zlib.html#zlib.DecompIO
keredson commentedon Mar 29, 2023
I abandoned my context manager wrapper for this singleton abomination:
At least it seems stable so far! 🤞😅
Curiously, the two
# WTFlines I tagged are necessary, otherwise the next alloc will fail. I don't have a working theory as to why this would be...EDIT 4/2/3023: Sadly not stable. Just less frequent GC misses.
Gadgetoid commentedon Mar 29, 2023
Wow, that's about as elegant as you can get I suppose. 😬
If nothing else your wrapper shows that
DecompIOprobably needs some work.Love your solution to allocating (what I guess is) a 32k buffer up front 😆
keredson commentedon Apr 1, 2023
a minimal example of garbage collection seemingly not working:
added a reset function to allow buffer reuse. see:
keredson commentedon Apr 2, 2023
proposed: keredson@7dc82cf
there's some housekeeping needed. arg checks. what to do if the wbits are different between the two streams.
also i can't figure out how to make it return none... 🤦 but it works! (and no more OOMs when parsing a few hundred small streams!)
keredson commentedon Apr 2, 2023
FYI this is the micropython git client i've been working on: https://github.com/keredson/ygit
Gadgetoid commentedon Apr 3, 2023
return mp_const_none;should do it.