1

I was testing early-returning in F# async and noticed some strange memory usage behaviour with MailboxProcessor. Consider this program:

let mp = MailboxProcessor<bool>.Start (fun inbox ->
    let mutable count = 0
    let rec await () = async {
        let! msg = inbox.Receive ()
        count <- count + 1
        if msg then
            printfn "Count: %4i | Queued: %4i" count inbox.CurrentQueueLength
            return! await ()
        else return! finish () }
    and finish () = async {
        printfn "done" }
    await ())
let numPosts = 10_000
for _ = 1 to numPosts do
    mp.Post true
    System.Threading.Thread.Sleep 1
mp.Post false
System.Console.ReadLine () |> ignore

If you delete the else, the memory usage keeps growing, which I expected, because the return! finish ()s keep piling up on the stack, as you can see with all of the dones which are printed upon completion. But if you put the else back in, thus making await tail-recursive, the memory usage still grows apparently indefinitely, although not as quickly, while the queue length remains at zero.

If you delete the Sleep line and raise numPosts to 100_000, the memory usage rises quickly with queue length but then stays more or less steady while the messages are processed, even remaining high after completion. This surprises me a little as well, as I would have expected the memory usage to decline with queue length.

Can anyone explain these memory usage behaviours?

4
  • How are you measuring memory usage? Are you sure that garbage collection is being triggered during the test? Commented Jul 19, 2023 at 18:00
  • @BrianBerns Just by watching the Memory column in Task Manager > Processes > search app name. I thought I didn't have to worry about garbage collection with .NET. Commented Jul 19, 2023 at 23:44
  • 2
    Garbage collection is automatic in .NET, but if you're going to track memory usage, you have to understand how it works. In particular, the garbage collector is what frees unused memory, so you probably want to invoke it explicitly in your loop via System.GC.Collect(). This will slow down the performance, but give you a better picture of memory usage. Commented Jul 20, 2023 at 2:02
  • 1
    GC doesn't immediately return freed memory to OS, instead it keeps it for later usage. It's not bad, because GC can react to OS's message about low remaining memory and free when it's needed. Also GC can return memory if didn't need that much in some amount of time. Commented Jul 20, 2023 at 5:59

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.