Skip to content

Commit 6ca941d

Browse files
Draft: Implement watching file paths via oslib.watch (#5068)
Currently watching files uses polling which is inefficient with large codebases. On my laptop `./mill --watch` uses 20% CPU just to watch the files for changes. This uses `oslib`'s `watch` module to watch for the file changes instead of polling them. We had to update oslib (com-lihaoyi/os-lib#386) to: - add a capability of filtering what folders it watches. This is needed because watching large generated folders like `out` or `.bloop` emits Java FS watcher `OVERFLOW` events. - remove the random `println`'s that `oslib.watch` had. The current approach to watching is to watch the workspace root via FS watching. FS watching only works with folders and we have to watch `root/build.mill`, and thus, `root/`, so everything else falls under it. Mac OS watcher performs watching recursively natively, but on linux oslib adds recursive watches itself, so we use `oslib.watch` filter to prevent watching anything that is not an ancestor of watched root. For example, if we have source at `root/module-a/src/`, we'll watch `root/`, `root/module-a/` and `root/`. Finally, events are filtered so that unrelated changes (like creating `root/random-file.txt`) does not trigger reevaluation. The fs watching is enabled by default and can be disabled via `--watch-via-fs-notify=false`. `0.12.x` version: #5073 --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent 80a96c7 commit 6ca941d

File tree

8 files changed

+238
-64
lines changed

8 files changed

+238
-64
lines changed

core/api/src/mill/api/Watchable.scala

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,22 @@ package mill.api
88
*/
99
private[mill] sealed trait Watchable
1010
private[mill] object Watchable {
11+
12+
/**
13+
* Watched path, can be watched via polling or via a notification system.
14+
*
15+
* @param p the path to watch
16+
* @param quick if true, only watch file attributes
17+
* @param signature the initial hash of the path contents
18+
*/
1119
case class Path(p: java.nio.file.Path, quick: Boolean, signature: Int) extends Watchable
20+
21+
/**
22+
* Watched expression, can only be watched via polling.
23+
*
24+
* @param f the expression to watch, returns some sort of hash
25+
* @param signature the initial hash from the first invocation of the expression
26+
* @param pretty human-readable name
27+
*/
1228
case class Value(f: () => Long, signature: Long, pretty: String) extends Watchable
1329
}

integration/invalidation/watch-source-input/src/WatchSourceInputTests.scala

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,9 @@ trait WatchTests extends UtestIntegrationTestSuite {
4444
val expectedShows0 = mutable.Buffer.empty[String]
4545
val res = f(expectedOut, expectedErr, expectedShows0)
4646
val (shows, out) = res.out.linesIterator.toVector.partition(_.startsWith("\""))
47-
val err = res.err.linesIterator.toVector
48-
.filter(!_.contains("Compiling compiler interface..."))
49-
.filter(!_.contains("Watching for changes"))
50-
.filter(!_.contains("[info] compiling"))
51-
.filter(!_.contains("[info] done compiling"))
52-
.filter(!_.contains("mill-daemon/ exitCode file not found"))
47+
val err = res.err.linesIterator.toVector.filter(s =>
48+
s.startsWith("Setting up ") || s.startsWith("Running ")
49+
)
5350

5451
assert(out == expectedOut)
5552

mill-build/src/millbuild/Deps.scala

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,9 @@ object Deps {
117117
val junitInterface = mvn"com.github.sbt:junit-interface:0.13.3"
118118
val commonsIo = mvn"commons-io:commons-io:2.18.0"
119119
val log4j2Core = mvn"org.apache.logging.log4j:log4j-core:2.24.3"
120-
val osLib = mvn"com.lihaoyi::os-lib:0.11.5-M8"
120+
val osLibVersion = "0.11.5-M8"
121+
val osLib = mvn"com.lihaoyi::os-lib:$osLibVersion"
122+
val osLibWatch = mvn"com.lihaoyi::os-lib-watch:$osLibVersion"
121123
val pprint = mvn"com.lihaoyi::pprint:0.9.0"
122124
val mainargs = mvn"com.lihaoyi::mainargs:0.7.6"
123125
val millModuledefsVersion = "0.11.4"

runner/daemon/package.mill

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ object `package` extends MillPublishScalaModule {
1919
def mvnDeps = Seq(
2020
Deps.sourcecode,
2121
Deps.osLib,
22+
Deps.osLibWatch,
2223
Deps.mainargs,
2324
Deps.upickle,
2425
Deps.pprint,

runner/daemon/src/mill/daemon/MillBuildBootstrap.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -275,7 +275,7 @@ class MillBuildBootstrap(
275275
// look at the `moduleWatched` of one frame up (`prevOuterFrameOpt`),
276276
// and not the `moduleWatched` from the current frame (`prevFrameOpt`)
277277
val moduleWatchChanged =
278-
prevOuterFrameOpt.exists(_.moduleWatched.exists(w => !Watching.validate(w)))
278+
prevOuterFrameOpt.exists(_.moduleWatched.exists(w => !Watching.haveNotChanged(w)))
279279

280280
val classLoader = if (runClasspathChanged || moduleWatchChanged) {
281281
// Make sure we close the old classloader every time we create a new

runner/daemon/src/mill/daemon/MillCliConfig.scala

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,11 @@ case class MillCliConfig(
102102
doc = """Watch and re-run the given tasks when when their inputs change."""
103103
)
104104
watch: Flag = Flag(),
105+
@arg(
106+
name = "notify-watch",
107+
doc = "Use filesystem based file watching instead of polling based one (defaults to true)."
108+
)
109+
watchViaFsNotify: Boolean = true,
105110
@arg(
106111
short = 's',
107112
doc =

runner/daemon/src/mill/daemon/MillMain.scala

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,21 @@ import mill.api.internal.{BspServerResult, internal}
44
import mill.api.{Logger, MillException, Result, SystemStreams}
55
import mill.bsp.BSP
66
import mill.client.lock.Lock
7-
import mill.constants.{OutFiles, DaemonFiles, Util}
8-
import mill.{api, define}
7+
import mill.constants.{DaemonFiles, OutFiles, Util}
98
import mill.define.BuildCtx
109
import mill.internal.{Colors, MultiStream, PromptLogger}
1110
import mill.server.Server
1211
import mill.util.BuildInfo
12+
import mill.{api, define}
1313

1414
import java.io.{InputStream, PipedInputStream, PrintStream}
1515
import java.lang.reflect.InvocationTargetException
1616
import java.util.Locale
1717
import java.util.concurrent.locks.ReentrantLock
1818
import scala.collection.immutable
1919
import scala.jdk.CollectionConverters.*
20-
import scala.util.{Properties, Using}
2120
import scala.util.control.NonFatal
21+
import scala.util.{Properties, Using}
2222

2323
@internal
2424
object MillMain {
@@ -344,9 +344,13 @@ object MillMain {
344344
if (config.watch.value) os.remove(out / OutFiles.millSelectiveExecution)
345345
Watching.watchLoop(
346346
ringBell = config.ringBell.value,
347-
watch = config.watch.value,
347+
watch = Option.when(config.watch.value)(Watching.WatchArgs(
348+
setIdle = setIdle,
349+
colors,
350+
useNotify = config.watchViaFsNotify,
351+
daemonDir = daemonDir
352+
)),
348353
streams = streams,
349-
setIdle = setIdle,
350354
evaluate = (enterKeyPressed: Boolean, prevState: Option[RunnerState]) => {
351355
adjustJvmProperties(userSpecifiedProperties, initialSystemProperties)
352356
runMillBootstrap(
@@ -356,8 +360,7 @@ object MillMain {
356360
streams,
357361
config.leftoverArgs.value.mkString(" ")
358362
)
359-
},
360-
colors = colors
363+
}
361364
)
362365
}
363366
}

0 commit comments

Comments
 (0)