4

I'm debugging a Haskell program using GHC's debugger (ghci) or via stack ghci, specifically with the Haskell GHCi Debug Adapter (Phoityne extension) in Visual Studio Code, and I'm encountering a major issue when stopping the debugger:

  • If my program enters a long-running loop or gets stuck in a computation, clicking the "Stop" icon in my debugger interface causes my computer to freeze.
  • The mouse and keyboard inputs become unresponsive, and eventually, the system slows down to the point where I cannot even open htop to identify which process is consuming resources.
  • I'm left with no option but to force a reboot.

I suspect that some resources (e.g., threads, memory allocations, or file handles) are not being properly released when stopping the debugger.

Some code

readMSet :: FilePath -> IO (MSet String)
readMSet fileName = do
    content <- readFile fileName
    let mset = processMSet content
    return mset

processMSet :: String -> MSet String
processMSet content =
    let ls = lines content
    in foldl add (MS []) ls -- Create the multiset

main = do
   m1 <- readMSet "path_to_file"

In particular, my debugger crashes in the main function, probably due to issues related to lazy evaluation. I suspect I might need to use DeepSeq to force evaluation, but that's not the principal issue for this topic.

System details

  • GHC Version / Stack: GHC 8.8.4 / 3.1.1
  • OS Version : Ubuntu 22.04.5 LTS
  • IDE : Visual studio code with the Haskell GHCi Debug Adapter (Phoityne extension)

Any insights or workarounds would be greatly appreciated! Is there a specific setup or practice for handling such situations in Haskell debugging?

What i'm expecting:

  • Is there a way to set a timer or timeout to automatically clean up (free) resources if a computation gets stuck in a loop or runs for too long?

  • Can I configure the debugger (or ghci) to safely terminate processes and free up system resources when I quit using the stop button?

What i've tried:

  • I've tried to manually interrupting with Ctrl+C, but the system still becomes unresponsive after stopping.
7
  • 1
    [1/3] One way to avoid this problem is specifying a heap size limit for GHC. Since you are invoking GHCi via Stack and the Phoityne extension, that would be done by adding --ghc-options="+RTS -M1G" (or whatever size is reasonable instead of 1G) to the stack ghci invocation specified by ghciCmd in the Phoityne configuration.
    – duplode
    Commented Dec 25, 2024 at 14:43
  • 1
    [2/3] The workaround is covered e.g. by Is there a way to limit the memory GHCi can have?. That Q&A, though, doesn't cover the issue of memory exhaustion after a successful Ctrl+C, something I also have observed on occasion in GHCi outside of VSCode.
    – duplode
    Commented Dec 25, 2024 at 14:44
  • 1
    [3/3] As for what is causing memory exhaustion, you most likely want to replace foldl with either foldl' (the strict left fold) or foldr, depending on exactly what you are doing. See e.g. foldl versus foldr behavior with infinite lists.
    – duplode
    Commented Dec 25, 2024 at 14:44
  • Thank you for your detailed and helpful response! [1/3] Heap size limit: The suggestion to specify a heap size limit with --ghc-options="+RTS -M1G" is exactly what I needed. I hadn't thought about controlling memory usage at the runtime system level—I'll definitely try this out. Commented Dec 25, 2024 at 15:05
  • [2/3]Thank you for linking the Stack Overflow discussion on memory limits in GHCi. It provides some valuable insights into this issue and gives me a better understanding of the underlying problem. Commented Dec 25, 2024 at 15:06

1 Answer 1

1

One way to prevent accidental memory exhaustion on GHCi is specifying a heap size limit for GHC. Since you are invoking GHCi via Stack and the Phoityne extension, that would be done by adding --ghc-options="+RTS -M1G" (or whatever size is reasonable instead of 1G) to the stack ghci invocation specified by ghciCmd in the Phoityne configuration.

I've tried to manually interrupting with Ctrl+C, but the system still becomes unresponsive after stopping.

My unproven theory is that this behaviour has to do with garbage collection. Since GC itself requires additional heap space, if the Ctrl-C is done too close to memory exhaustion, GHC's very attempt to reclaim memory might push it over the edge. If my hunch is correct, GHC issue #24398, in which a heap overflow in GHCi with a heap size limit is followed by a second overflow which brings down GHCi, could well have the same underlying cause. (In fact, it could be worth it to report the observations here in that issue! I'll look into doing that.)


In particular, my debugger crashes in the main function, probably due to issues related to lazy evaluation. I suspect I might need to use DeepSeq to force evaluation, but that's not the principal issue for this topic.

The immediate problem is almost certainly the use of foldl, as discussed in foldl versus foldr behavior with infinite lists. When dealing with lists, the rule of thumb is to use foldr if the fold result will be consumed lazily, and foldl' (the strict left fold) if it won't. In your case, building up a multiset requires evaluating the elements being inserted and their multiplicities anyway, so foldl' should be the better choice. One important caveat, though, is that effective use of foldl' calls for strictness in the result being built up. That being so, you'll probably find it advantageous to implement your multiset in terms of something less lazy than vanilla lists — a map using the Data.Map.Strict could be a good option. Those changes will likely make it unnecessary to reach for deepseq. For further discussion of this matter, see Tom Ellis' Make invalid laziness unrepresentable.

2
  • Thank you for improving your explanations! For debugging or pre-deployment execution, minimizing heap that the process use is advisable, as reckless memory use can lead to strange behavior with the garbage collector for the process (e.g., GHC Issue #24398). Using strict foldl solved my problems, making deepseq unnecessary. By the way, does each GHCi instance have its own heap? Is there a way to set a max heap size for GHCi itself, not just the running process (e.g., ghci --ghc-options="+RTS -M1G" target for 1G max)? Commented Dec 27, 2024 at 16:01
  • 1
    @DanieleCaliandro I don't think so. Each GHCi session corresponds to a separate ghc process, which is ultimately what we set heap size and other options for.
    – duplode
    Commented Dec 27, 2024 at 21:46

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.