asterius icon indicating copy to clipboard operation
asterius copied to clipboard

Large closures sometimes incorrectly garbage-collected

Open acrylic-origami opened this issue 5 years ago • 1 comments

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.

  1. Given payload :: String defined 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 ()
    
  2. With a generated payload string instead, say:

    payload = take 900000 $ concat $ repeat "01" -- repeat '0' seems to be optimized away
    

    instead, barf is 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.

  3. 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 through window. 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.

acrylic-origami avatar Jun 20 '20 02:06 acrylic-origami

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.

blynn avatar Jun 07 '21 21:06 blynn