weakref: Don't leak cyclic weakrefs.#21
weakref: Don't leak cyclic weakrefs.#21AJMansfield wants to merge 11 commits intodpgeorge:py-add-weakreffrom
Conversation
The behaviour follows CPython. It adds a new WTB table to the GC to efficiently track which heap pointers have a weak reference attached. Enabled at the "everything" level. Signed-off-by: Damien George <damien@micropython.org>
Signed-off-by: Damien George <damien@micropython.org>
Needs a native exp file because native code doesn't print line numbers in the traceback. Signed-off-by: Damien George <damien@micropython.org>
Following a69425b. Signed-off-by: Damien George <damien@micropython.org>
Signed-off-by: Damien George <damien@micropython.org>
The amount of GC heap got smaller because weakref WTB takes some of the available RAM. Signed-off-by: Damien George <damien@micropython.org>
Signed-off-by: Damien George <damien@micropython.org>
It takes longer now that weakref is enabled in the coverage build. Signed-off-by: Damien George <damien@micropython.org>
Signed-off-by: Damien George <damien@micropython.org>
Signed-off-by: Anson Mansfield <amansfield@mantaro.com>
Signed-off-by: Anson Mansfield <amansfield@mantaro.com>
|
Thanks for this, it's interesting. But consider the following case: a Eg: l = []
def callback(arg):
pass
file = <some opened file>
weakref.finalize(l, callback, file)
gc.collect() # will close the file!
gc.collect() # will close the file again!
l = None
gc.collect() # file is closed again, callback runs, everything is reclaimedAm I correct about what will happen there? |
|
The Though, you're right that there's still the possibility of this being unexpected if the finaliser's file argument ends up closed even once before the finaliser runs. |
Ah, yes, you're right. It'll only run once. But still, it's left in a kind of half-finalised state. And application code would definitely not expect a file to be closed before it's really ready to be GC'd. Eg the |
|
Took me a moment of puzzling through how or even whether micropython#18840 avoids that, too --- the reference inserted into the target's TBH it's probably worth testing the comparison to CPython here too --- this feels like a limitation that could easily be present there too. |
Yes, that's a good point, it looks like micropython#18840 can finalise args/kwargs before running
Looking at CPython's implementation, it keeps strong references to all |
5955972 to
0d6ff50
Compare
Summary
In micropython#18822 (review), I discussed how that PRs implementation of weakref leaks weakrefs whose callbacks are closures of their target, with a discussion of the underlying mathematics and one primitive way to resolve this issue.
This PR proposes a more refined version of that resolution, achieved by moving
mp_weakref_maptable from the root pointer section ofmp_state_ctx.vmto a non-root-pointer part ofmp_state_ctx.memand adding it to the GC's root closure as a separate step, so that membership in that table is only considered for thegc_sweep_free_blocksstep, and not for thegc_sweep_run_finalisersstep.Testing
This PR adds an automated test that verifies the lack of a leak (with very loose bounds). I've checked this test for sensitivity against Unix, and can confirm that it passes on Unix and RP2.
I've rigorously checked the underlying mathematical theory. While it's not impossible that I've made an error in my proofs I'm confident that this change is theoretically sound --- this change maintains the GC's "conservativeness" guarantee and still prevents python code from forming an invalid.
Trade-offs and Alternatives
Rerunning a root-collection pass from this code region does come with a minor increase to code size and runtime; however, since this is able to re-use functionality that already existed in the garbage collector this extra increase should be minimal.