Skip to content

Store schedulerThread in StablePtr.#1540

Merged
dmjio merged 4 commits into
masterfrom
kill-scheduler-for-real
Jun 19, 2026
Merged

Store schedulerThread in StablePtr.#1540
dmjio merged 4 commits into
masterfrom
kill-scheduler-for-real

Conversation

@dmjio

@dmjio dmjio commented Jun 19, 2026

Copy link
Copy Markdown
Owner

When using live and reload functions, the scheduler ThreadId needs to persist between :r invocations (via StablePtr). Otherwise, the CAF gets recreated and the previous scheduler thread (along with its closure) leaks into the heap (due to the forever call). This causes accumulated heap growth during a hot reload session. By storing the ThreadId in the StablePtr this allows us to kill the previous scheduler ThreadId cleanly.

Likewise the ComponentState, in order to be reified reliably, needs to be fetched by unique Key (not ComponentId, which is not stable between user changes to application state). Onus falls on the user to Key their Component uniquely during a hot-reload session (with the exception of the top level ComponentId which is always 1).

This means live users should always Key Component uniquely. The only Component that is allowed to be left unkeyed during hot-reload (w/ live) is the top-level root Component, of id 1.

  • Add ThreadId to StablePtr in live / reload (along with components :: IORef (CompenentState parent props model action))
  • Don't clearPage during live, just clearBody. Scripts / styles only need to be loaded once.
  • Drop killThread =<< readIORef schedulerThread
  • Add _componentKey to ComponentState.

The scheduler `ThreadId` needs to also persist
between `:r` invocations. Otherwise, the CAF gets
recreated and the previous scheduler thread (along
with its closure) leaks, causing accumulating heap
growth over time.

Also, the `ComponentState`, in order to be reified reliably
between each `:r`, needs to be fetched by unique `Key`.

This means for users, when using `live`, they should always `Key`
`Component` uniquely. The only `Component` that is allowed to be left
unkeyed during hot-reload (w/ `live`) is the top-level root
`Component`, of id `1`.

- [x] Add `ThreadId` to `StablePtr` in `live` / `reload` (along with
`components :: `IORef (CompenentState parent props model action)`

- [x] Don't `clearPage` during `live`, just `clearBody`. Scripts /
styles only need to be loaded once.

- [x] Drop `killThread =<< readIORef schedulerThread`

- [x] Add `_componentKey` to `ComponentState`.
@dmjio

dmjio commented Jun 19, 2026

Copy link
Copy Markdown
Owner Author

Consider constraining model on Generic

dmjio added 3 commits June 19, 2026 11:36
When using `evalIO` in the playground (`try-miso`), the `GHCi` session is
reused and the CAFs in miso lib do not get cleared between `Main.hs`
reloads (`schedulerThread` lingers). When using WASM browser mode
(GHCi :r mode) the CAFs in miso do get cleared (hence the
need for the StablePtr, and C heap storage).

So we still need to explicitly kill the Thread in `initComponent`
(this logic was previously moved to `live` / `reload`). The
`killThread` call on an already dead thread is a no-op
(occurs w/ `live` and `reload` functions) but when using `startApp`
with `evalIO` (on the playground) it will actually kill the old
thread if a reload is detected (`IM.size components > 0`).
`ComponentId` is not a stable identifier across reloads. `Key`
is more accurate. Unkeyed Component don't get hot reload benefits.

Changing component Key during interactive editing w/ live will still
segfault. This can be solved by using a Generic diff.
@dmjio
dmjio merged commit 7adaa47 into master Jun 19, 2026
3 checks passed
@dmjio
dmjio deleted the kill-scheduler-for-real branch June 19, 2026 20:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant