Leak, another one
zevv opened this issue · comments
For @Clyybber
Here's a little puzzle. The program below outputs this:
done
destroyed noleak one
Now go uncomment the commented line, what do you expect it would print?
import cps, deques
type
C = Continuation
ThingObj = object
name: string
Thing = ref ThingObj
proc `=destroy`*(t: var ThingObj) =
echo "destroyed ", t.name
proc foo_leak(name: string): Thing {.cps:C.}=
Thing(name: name)
proc foo_noleak(name: string): Thing =
Thing(name: name)
proc foo1() {.cps:C.} =
let c1 = foo_noleak("noleak one")
# let c2 = foo_leak("leak one") # <-- try uncommenting out this line
echo "done"
var queue: Deque[Continuation]
block:
queue.addLast whelp foo1()
while queue.len > 0:
var c = queue.popFirst
discard c.trampoline
Heavily minimized:
type
ThingObj = object
name: string
Thing = ref ThingObj
C = ref object of RootObj
fn: proc(c: C): C {.nimcall.}
mom: C
result: Thing
c2: Thing
child: C
proc `=destroy`(t: var ThingObj) =
echo "destroyed ", t.name
proc trampoline(c: C): C =
var c: C = c
while not c.isNil and not c.fn.isNil:
c = c.fn(c)
result = c
proc foo_leak(continuation: C): C =
echo "hey"
continuation.result = Thing(name: "leak one")
continuation.fn = nil
result = continuation.mom
#continuation.mom = nil # Break cycle; fix leak
proc call(continuation: C): Thing {.used.} =
result =
if continuation.fn == nil: continuation.result
else: (trampoline continuation).call
proc finish(continuation: C): C {.nimcall.} =
echo ["done"]
continuation.fn = nil
result = continuation
proc postChild(continuation: C): C {.nimcall.} =
continuation.c2 = continuation.child.call
continuation.fn = finish
return continuation
proc foo1(continuation: C): C =
continuation.fn = postChild
continuation.child = C(fn: foo_leak, mom: continuation)
return continuation.child
proc main =
var c = C(fn: foo1)
discard c.trampoline
echo cast[int](c)
echo cast[int](c.child.mom) # Cyclic reference to c
main()
c.child.mom
pointing to c
creates a cycle.
Ok, but given that I did not make the mistake of spawning the continuations in the global scope, why does this even leak when using orc?
It's an orc bug nim-lang/Nim#18421.
The cycle itself comes from this line in the minimized example continuation.child = C(fn: foo_leak, mom: continuation)
which corresponds to cps environment_402654022(continuation).child_402654055 = cps environment_402653363(tail(Continuation(continuation), C(whelp_402653376("leak one"))))
in the cps generated code.
Upstream bug was fixed, leaving this open though, since I believe we could get rid of the cycle, which is better than relying on the cycle collector.
How do you propose to get rid of the cycle?
In the minimized snippet zeroing out mom
works (the commented out statement), but not sure if that would be correct.
Well, if we don't store the parent in the child continuation, how can we unwind the stack and return the the parent after the child has completed?
Similarly, if we don't store the child in the parent, we cannot, for example, fetch the result of the child when the parent resumes.