Elm programs that don't die

There are times when you want to kill an Elm program good and proper. A reason for this is that Elm programs don’t always clean themselves up how you would expect. Especially if you are using Elm in a fairly custom way like with a framework.

Djelm, a framework I built for seamlessly using Elm in the Django web framework, does a few tricks and sleight of hand to achieve its goal of hydrating Elm programs client side from server generated html Django pumps out.

If you have a simple MPA, there are no issues at all, you load a page, the page resets the state, djelm runtime hydrates any included Elm programs, happy days!

If however you use a slightly more modern approach like HTMX to get that sweet SPA experience then things kinda start hanging around. You load a page, but now HTMX gets involved and instead of a fresh new page, you get the same page you were on, just with the new html spliced in and the url changed to reflect the new route. It’s wonderful but here be dragons.

What happened to the Elm programs from the previous page? Well, there was no fresh page loaded so those programs are actually still alive and holding on to memory, it’s just that they aren’t connected to DOM. If one of those programs was fetching data from the server every 5 seconds then the chances are, it still is!

Frameworks like elm-pages and Lamdera almost certainly have to deal with this scenario, as does djelm.

Djelm works around this by patching compiler output at bundle time. Internally djelm uses Parcel so I wrote @confidenceman02/parcel-transformer-djelm to do the job. Parcel simply calls my transformer when it sees Elm code then the transformer handles the compilation and patching. This was massively inspired by the very excellent @parcel/transformer-elm

What it does is adds a ‘die’ hook, an idea I stole from this gist with some modifications. Now, the program’s internal references can be nulled out and memory garbage collected. You can read more about it in djelm’s JS interop section in the README.

Oddly enough it’s been a real treat working on this. It takes you in to the parts of Elm that are less visible but so very interesting.

5 Likes

Cool!

I happen to have been working on stopping Elm programs recently, too. I want to add it to the elm/* packages directly, no patching needed. While doing that, I learned that the die function from the gist you linked treats symptoms rather than the underlying issue. But that’s better than nothing! I’ll get back to that below.

At the same time, I’ve worked on bringing the hot reloading capability from elm-watch to elm/* packages, so all tools – including yours – can benefit from it. I saw you have an issue about adding elm-hot so I thought you’d be interested in that, too.

I’ve built the app hot reloading and stopping on top of my elm-safe-virtual-dom project (which I haven’t announced fully yet, but anyway). Follow that link to learn how forked elm/* packages can be installed.

I’m currently integrating my work with Lamdera. The Lamdera team has expressed interest in shipping my forked elm/* packages by default. Note that the Lamdera Compiler is open source and can be used for vanilla Elm programs. Several community members have started contributing to it.

All in all, there is a potential future where you get both app stopping and hot reloading for free (with a side bonus of a virtual DOM that doesn’t crash as easily). Maybe via the Lamdera Compiler.

Now, back to “treating the symptom rather than the underlying issue”. A Platform.worker program without any ports (yes, that’s a useless program, but anyway) can actually stop already. But anything other than that have a few problems:

For more details, read how app stopping and hot reloading works.

I plan to make PR:s to the elm/* repos when I feel finished, for increased visibility. (I’ve already created PR:s for my elm-safe-virtual-dom stuff.)

Finally, in case someone is curious how to test and debug garbage collection: Use Chrome’s Memory devtools. I put a ridiculously big field in the model (List.range 0 100000 |> List.map (\i -> { a = String.repeat i "a", b = i }), around 50 MiB) and check if memory usage goes down after stopping the app. If not, I take a memory snapshot, sort objects by memory size and check the “Retainers” to see what’s holding on to the model. It’s not super easy, but much better than shooting in the dark.

12 Likes

Thanks so much for this great explanation, it was thorough and very well documented. Really impressive work!

Im definitely going to steal the VirtualDom_removeAllEventListenersHelp(tNode) code as that was absolutely a head scratcher for me.

Adding the $__shutdown on the stepper is super clever and that means I can just call it from my die code and remove all event listeners from the tree, which is a huge improvement.

It would be lovely to not need to brute force the code in to the compiler output so I’m definitely keen for a “it just works” solution. Especially HMR.

I’ll follow your progress!

2 Likes

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.