Large closures sometimes incorrectly garbage-collected
Describe the bug A clear and concise description of what the bug is.
Callbacks created by wrapper, along with their dependencies, seem to be susceptible to garbage collection. Minimally, a single callback that closes over a large value can trigger a GC and subsequent attempts to call it triggers the ReentrancyGuard. This can be alleviated by growing --gc-threshold, although I suspect that if the memory consumption overtakes this limit during the application lifetime, then the callbacks will start disappearing again.
To Reproduce Steps to reproduce the behavior.
I've found a few different behaviors depending on what the "large value" is and how it's closed over. All of them are fixed by raising --gc-threshold.
-
Given
payload :: Stringdefined as a large string literal (900K characters did the trick for the default--gc-threshold=64, at least on Firefox 77 and Chrome 83), the following triggered ReentrancyGuard after a few intervals:callback :: String -> Int -> Int callback p _ = fromEnum $ last p -- `last p` is itself constant-memory with optimizations (GHC \#10260) -- just to force the whole string to be loaded, as `head p` doesn't repro the error main = interval_export =<< mk_callback (callback payload) foreign import javascript unsafe "wrapper" mk_callback :: (Int -> Int) -> IO JSFunction foreign import javascript unsafe "(() => { window.setInterval(_ => $1(0).then(console.log, console.log), 100) })()" interval_export :: JSFunction -> IO () -
With a generated
payloadstring instead, say:payload = take 900000 $ concat $ repeat "01" -- repeat '0' seems to be optimized awayinstead,
barfis triggered. This sounds like it's correctly catching a memory limit issue. I'm not familiar with the maximum value sizes that are behind this to know for sure, but this is on track with my expectations if I were to hit a memory limit. -
In my original use case, I
fetched a sizeable JSON string (167KB), shuttled it into a Haskell datatype, and partially applied a function with it, exposing it to JS throughwindow. While the first call succeeded (since it hadn't forced the JSON parsing until then) the subsequent calls would either hang or raise ReentrancyGuard.
Expected behavior A clear and concise description of what you expected to happen.
If I'm hitting a memory limit, I'd hope for an error message related to memory consumption, rather than having the callback swept away by GC.
Environment
- OS name + version: macOS Mojave (10.14.2)
- Version of the code: Asterius docker image 018c2194f998 (June 16, 2020, from terrorjack/asterius)
Additional context Add any other context about the problem here.
My demo at https://crypto.stanford.edu/~blynn/compiler/fol.html might be running into this bug. The source: https://crypto.stanford.edu/~blynn/compiler/fol.lhs. Solving simpler problems works fine, and I can repeatedly solve the same one by clicking on a button many times.
Every makeHaskellCallback call applies to a one-shot function, but if I use "wrapper oneshot" then my solver crashes on larger problems, e.g. preset "steamroller" with "Lazy MESON".
I worked around this issue by omitting the "oneshot", but then the second time I click "Lazy MESON" on "steamroller" it crashes after clearing the textboxes.
I also tried introducing an IORef that collects all the one-shot callbacks in a list, and calling mapM_ freeHaskellCallback on its contents at the end of a run. Like above, the first run succeeds, but the second time I click the button the program crashes after clearing the textboxes.