Skip to content

Fix problems with watch mode with --workers=1 - #683

Merged
lydell merged 3 commits into
masterfrom
fix-single-threaded-watch-mode
Aug 1, 2026
Merged

Fix problems with watch mode with --workers=1#683
lydell merged 3 commits into
masterfrom
fix-single-threaded-watch-mode

Conversation

@lydell

@lydell lydell commented Aug 1, 2026

Copy link
Copy Markdown
Collaborator

Followup on #680.

There were a couple of problems with single-threaded watch mode (--workers=1 --watch):

  1. If you made changes to Elm files, the tests were re-run, but with the old code. Fixed by clearing the require cache.
  2. When the above was fixed, loading the new code resulted in an error due to the IIFE wrapper around Elm’s JS in before.js was wrong.
  3. When the above was fixed, I noticed that we leaked memory each run.

To measure memory usage, I used the following patch:

diff --git a/elm/src/Test/Runner/Node.elm b/elm/src/Test/Runner/Node.elm
index 679d7e3..5bb4b09 100644
--- a/elm/src/Test/Runner/Node.elm
+++ b/elm/src/Test/Runner/Node.elm
@@ -63,9 +63,22 @@ type alias Model =
     , processes : Int
     , nextTestToRun : TestId
     , autoFail : Maybe String
+    , heavy : List Heavy
     }
 
 
+type alias Heavy =
+    { name : String
+    , hydrospanner : Int
+    , falcon : Falcon
+    , ints : List Int
+    }
+
+
+type Falcon
+    = Millenium Float
+
+
 {-| A program which will run tests and report their results.
 -}
 type alias TestProgram =
@@ -245,6 +258,19 @@ sendBegin model =
         |> elmTestPort__send
 
 
+heavy : List Heavy
+heavy =
+    List.range 0 1000
+        |> List.map
+            (\i ->
+                { name = String.fromInt i
+                , hydrospanner = i ^ 2
+                , falcon = Millenium (toFloat i * pi)
+                , ints = List.range 0 i
+                }
+            )
+
+
 init : InitArgs -> Int -> ( Model, Cmd Msg )
 init { processes, globs, paths, fuzzRuns, initialSeed, report, runners } index =
     let
@@ -290,6 +316,7 @@ init { processes, globs, paths, fuzzRuns, initialSeed, report, runners } index =
             , results = []
             , testReporter = testReporter
             , autoFail = autoFail
+            , heavy = heavy
             }
 
         cmd =
@@ -324,6 +351,7 @@ failInit message report _ =
             , results = []
             , testReporter = createReporter report
             , autoFail = Nothing
+            , heavy = heavy
             }
 
         cmd =
diff --git a/lib/RunTests.js b/lib/RunTests.js
index 7da7be8..9454c19 100644
--- a/lib/RunTests.js
+++ b/lib/RunTests.js
@@ -297,6 +297,7 @@ function runTests(
         if (!Report.isMachineReadable(report)) {
           console.log(chalk.blue('Watching for changes...'));
         }
+        global.gc();
         currentRun = undefined;
       }
     };
@@ -352,7 +353,15 @@ function runTests(
     // It’s unclear when this event occurrs.
     watcher.on('error', (error) => console.error('Watcher error:', error));
 
-    currentRun = run().then(onRunFinish);
+    currentRun = run()
+      .then(onRunFinish)
+      .then(() => {
+        setInterval(() => {
+          process.stdout.write(
+            ` ${(process.memoryUsage().heapUsed / (1024 * 1024)).toFixed(2)}\r`
+          );
+        }, 1000);
+      });
 
     // A promise that never resolves. We’ll watch until killed.
     return new Promise(() => {});

It puts a ~30 MB object in the Elm model. This makes it easy to see if the memory usage is growing with each run.

After each run I force garbage collection. (Requires --expose-gc.) Every second I measure used heap space.

Before, the memory usage increased with ~30 MB per run. After, the memory usage is unchanged after each run.

Run it like so:

cd example-application
node --expose-gc ../bin/elm-test --seed 0 --workers 1 --watch

Then make a change to one of the test files (or touch it) to trigger new runs.

lydell added 3 commits August 1, 2026 19:09
Previously, we were accidentally writing `Elm` to the global object,
and Elm has checks in place for this:

> Your page is loading multiple Elm scripts with a module named Elm.Test.Generated.Main. Maybe a duplicate script is getting loaded accidentally? If not, rename one of them so I know which is which!

This commit fixes the wrapper around the Elm JS code so that we write to
a temporary, local object instead.
@lydell
lydell merged commit 76fb246 into master Aug 1, 2026
13 checks passed
@lydell
lydell deleted the fix-single-threaded-watch-mode branch August 1, 2026 17:23
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