Dev log: Hot reload and browser visibility

Today I tackled a bug in my development environment where my game would freeze up, all animations stopping, after every time I saved a file. This only started happening the day I started using Lamdera’s freeze mode. Freeze mode promised to save me time by preventing my frontend state from resetting back to a fresh init after every file change/hot reload, but this darn animation bug was bad enough I would have to do without it if I couldn’t fix it!

TLDR: Hot reload triggers the Elm.Browser.onVisibilityChange to set visibility to Hidden, but does not ever set it back to Visible. This means my app thinks it’s not visible when it is and pauses all animations. Workaround: disable visibility change logic in dev mode.

Investigation

It all started when I added a login page to my game. My development loop went from:

Make change → Tab over to browser → See change

to…

Make change → Tab over to browser → Log in anonymously → See change

Sometimes the intermediate steps would be more than just logging in anonymously, but adding that extra step is what made the whole thing intolerable.

I brought up this pain point in the Lamdera channel of Elm Slack and was helped promptly by a core member of the Lamdera team, Martin Stewart, who pointed out that in front of my nose this whole time has been the “freeze mode”. Freeze mode prevents hot reloads from resetting frontend state, exactly what I needed. Armed with this functionality, my dev loop was about to not only get directly around the login step but get better than it ever had been before!

The reality was that I had created a new bug for myself with freeze mode, and it sank in rather quickly. Any continuous animation (e.g. studying a skill) would pause for no reason after a hot reload, and seemed not to be able to be restarted. Some quick debugging revealed that the animation timers weren’t truly paused, it was the onAnimationFrame handler aborting early because it thought the app was in the background [0].

From there, my understanding of the bug went through a few iterations of understanding:

  • This is a bug with Lamdera Freeze mode somehow resetting my app visibility state to Hidden. No wait, my init actually sets it to Visible, plus the whole point of Freeze mode is it doesn’t call init
  • Oh, it’s that in Freeze mode the onVisibilityChange event from Elm fires as Hidden for an unload but not Visible upon reload of the page! Except adding a Debug.log to the visibility change handler reveals it behaves the same way with Freeze mode off…
  • So what’s going on is that in a sense Elm’s Browser.Events.onVisibilityChange code is buggy when used with Lamdera’s hot reload? I read the source and it looks like it watches document.visibilityState value, but maybe that isn’t working?

After a longer reflection, I have to somewhat unsatisfyingly conclude that neither Elm’s visibility change logic nor Lamdera’s freeze mode/hot reload logic is buggy; the bug sadly lies in the combination. The essence of the bug is that browser visibility goes from Hidden → Visible without Browser.Events.onVisibilityChange ever firing. Because the runtime of Elm on the “other side” of a hot reload can’t know that there even was a previous value for that info, and correctly won’t fire a change event for it. My app is stuck in the middle, being told it’s not visible, and never being told it became visible again.

The freeze mode seems to promise full continuity of state before and after the page reload occurs, and in a sense it delivers on this promise. The sense in which it doesn’t is that browsers have their own state, a fact from which Elm is usually able to shield the user. The Lamdera docs on freeze mode hint at this class of bug with their statement:

Note: the frontend init won’t run when freeze is on – so remember this if you’re using Browser.Dom Cmds to manipulate the page on init in normal mode, or relying on some sendToBackend-on-init behaviour – for that you want to go back to normal mode.

But what they mention there isn’t exactly what happened to me-- no Browser.Dom Cmds are missing, what’s missing is state that lives only in the browser-- document.visibilityState.

Solutions

The best solution to this would be to find a way to issue a Cmd upon hot reload, the same way Cmds are issued upon init. I could use this to force the app back to visible. Given the warning in the docs, it doesn’t sound like Lamdera is planning on adding this, but something approximating it could work.

Another solution would be to not use onVisibilityChange. If there were a Cmd that fetched browser attributes like visibilityState, I could use that to get the visibility before each animation frame and not need to track it in state at all. This has a certain elegance, but a brief search indicates nothing like it exists. It could be done with ports, but those have friction to implement, make things much more complex, and are experimental in Lamdera anyway.

The dumbest solution is the one for me, right now. I have added a line to my visibility change handler to ignore the event if in dev mode-- I will have to remove this if I ever need to manually test functionality relying on the visibility logic, but for most development it will be completely fine.

[0] Tracking the visibility status of my app was a feature I implemented long ago. As an Ultra Idle game, backgrounding the app to do other stuff was expected to be an extremely common occurrence, and we a) didn’t want to run pointless calculations when in the background, and b) wanted to show the user a modal showing their progress while the tab was hidden upon return. To achieve these goals, we used Browser.Events.onVisibilityChange. This handy subscription notifies your app when its visibility changes and I store the latest value in the FrontendModel.

1 Like

Thanks for the detailed writeup @tristanp , and sorry for the goose chase!

Hoping we’ll be able to integrate @lydell 's contributions to improving the resiliency of app live reloads into the lamdera live experience soon, which I think will likely fix this issue by avoiding the page reload and becoming more of a “true” hot reload :slight_smile:

2 Likes