Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

improvement: ignore create or modify file watcher event when content did not change #7006

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 19 additions & 13 deletions metals/src/main/scala/scala/meta/internal/metals/Compilations.scala
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ final class Compilations(
downstreamTargets: PreviouslyCompiledDownsteamTargets,
bestEffortEnabled: Boolean,
)(implicit ec: ExecutionContext) {
private val fileSignatures = new SavedFileSignatures
private val compileTimeout: Timeout =
Timeout("compile", Duration(10, TimeUnit.MINUTES))
private val cascadeTimeout: Timeout =
Expand Down Expand Up @@ -107,24 +108,28 @@ final class Compilations(
compileBatch(targets).ignoreValue
}

def compileFile(path: AbsolutePath): Future[b.CompileResult] = {
def empty = new b.CompileResult(b.StatusCode.CANCELLED)
for {
targetOpt <- expand(path)
result <- targetOpt match {
case None => Future.successful(empty)
case Some(target) =>
compileBatch(target)
.map(res => res.getOrElse(target, empty))
}
_ <- compileWorksheets(Seq(path))
} yield result
def compileFile(path: PathWithContent): Future[Option[b.CompileResult]] = {
if (fileSignatures.didSavedContentChanged(path)) {
def empty = new b.CompileResult(b.StatusCode.CANCELLED)
for {
targetOpt <- expand(path.path)
result <- targetOpt match {
case None => Future.successful(empty)
case Some(target) =>
compileBatch(target)
.map(res => res.getOrElse(target, empty))
}
_ <- compileWorksheets(Seq(path.path))
} yield Some(result)
} else Future.successful(None)
}

def compileFiles(
paths: Seq[AbsolutePath],
pathsWithContent: Seq[PathWithContent],
focusedDocumentBuildTarget: Option[BuildTargetIdentifier],
): Future[Unit] = {
val paths =
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we could just find first that changed in a given target?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, though I doubt it ever too many files at once. Only file watcher passes more than one file.

pathsWithContent.filter(fileSignatures.didSavedContentChanged).map(_.path)
for {
targets <- expand(paths)
_ <- compileBatch(targets)
Expand Down Expand Up @@ -157,6 +162,7 @@ final class Compilations(
lastCompile = Set.empty
cascadeBatch.cancelAll()
compileBatch.cancelAll()
fileSignatures.cancel()
}

def recompileAll(): Future[Unit] = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -750,8 +750,9 @@ abstract class MetalsLspService(
.getOrElse(current)
}

val content = FileIO.readAllBytes(path)
// Update md5 fingerprint from file contents on disk
fingerprints.add(path, FileIO.slurp(path, charset))
fingerprints.add(path, new String(content, charset))
// Update in-memory buffer contents from LSP client
buffers.put(path, params.getTextDocument.getText)

Expand Down Expand Up @@ -784,7 +785,10 @@ abstract class MetalsLspService(
Future
.sequence(
List(
maybeCompileOnDidFocus(path, prevBuildTarget),
maybeCompileOnDidFocus(
PathWithContent(path, content),
prevBuildTarget,
),
compilers.load(List(path)),
parser,
interactive,
Expand Down Expand Up @@ -821,20 +825,20 @@ abstract class MetalsLspService(
CompletableFuture.completedFuture(DidFocusResult.RecentlyActive)
} else {
worksheetProvider.onDidFocus(path)
maybeCompileOnDidFocus(path, prevBuildTarget).asJava
maybeCompileOnDidFocus(PathWithContent(path), prevBuildTarget).asJava
}
}

protected def maybeCompileOnDidFocus(
path: AbsolutePath,
path: PathWithContent,
prevBuildTarget: b.BuildTargetIdentifier,
): Future[DidFocusResult.Value] =
buildTargets.inverseSources(path) match {
buildTargets.inverseSources(path.path) match {
case Some(target) if prevBuildTarget != target =>
compilations
.compileFile(path)
.map(_ => DidFocusResult.Compiled)
case _ if path.isWorksheet =>
case _ if path.path.isWorksheet =>
compilations
.compileFile(path)
.map(_ => DidFocusResult.Compiled)
Expand Down Expand Up @@ -938,16 +942,22 @@ abstract class MetalsLspService(
}

protected def onChange(paths: Seq[AbsolutePath]): Future[Unit] = {
paths.foreach { path =>
fingerprints.add(path, FileIO.slurp(path, charset))
}
val pathsWithContent =
paths.map { path =>
val content = FileIO.readAllBytes(path)
fingerprints.add(path, new String(content, charset))
PathWithContent(path, content)
}

Future
.sequence(
List(
Future(indexer.reindexWorkspaceSources(paths)),
compilations
.compileFiles(paths, Option(focusedDocumentBuildTarget.get())),
.compileFiles(
pathsWithContent,
Option(focusedDocumentBuildTarget.get()),
),
) ++ paths.map(f => Future(interactiveSemanticdbs.textDocument(f)))
)
.ignoreValue
Expand All @@ -958,7 +968,10 @@ abstract class MetalsLspService(
.sequence(
List(
compilations
.compileFiles(List(path), Option(focusedDocumentBuildTarget.get())),
.compileFiles(
List(PathWithContent.deleted(path)),
Option(focusedDocumentBuildTarget.get()),
),
Future {
diagnostics.didDelete(path)
testProvider.onFileDelete(path)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -635,7 +635,9 @@ class ProjectMetalsLspService(
for {
_ <- connect(ImportBuildAndIndex(session))
} {
focusedDocument().foreach(path => compilations.compileFile(path))
focusedDocument().foreach(path =>
compilations.compileFile(PathWithContent(path))
)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package scala.meta.internal.metals

import java.io.IOException

import scala.collection.concurrent.TrieMap

import scala.meta.internal.io.FileIO
import scala.meta.internal.metals.MetalsEnrichments._
import scala.meta.internal.mtags.MD5
import scala.meta.io.AbsolutePath

class SavedFileSignatures {
private val previousCreateOrModify = TrieMap[AbsolutePath, String]()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we reuse MutableMd5Fingerprints somehow? Maybe just save them in didSave with a flag to MutableMd5Fingerprints and then we can get the last saved one instead of calculating again?

We can still have a separate collection in Compilations so that it's possible to clear it and force new compilations?

Copy link
Contributor Author

@kasiaMarek kasiaMarek Dec 6, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For didSave we put them anyway there but for file watcher event like CreateOrModify we don't, and we don't want to since it stores content, which we don't need. Also there is no way to check that the caller of compileFile/s saved the fingerprint (there may be an old one there), so it becomes some hidden knowledge that you should.
Edit: we can potentially use flags and mark "used ones"


def didSavedContentChanged(pathWithContent: PathWithContent): Boolean = {
val path = pathWithContent.path
pathWithContent
.getSignature()
.map { md5 =>
synchronized {
if (previousCreateOrModify.get(path) == md5) false
else {
md5 match {
case None => previousCreateOrModify.remove(path)
case Some(md5) => previousCreateOrModify.put(path, md5)
}
true
}
}
}
.getOrElse(true)
}

def cancel(): Unit = previousCreateOrModify.clear()
}

class PathWithContent(
val path: AbsolutePath,
optContent: Option[PathWithContent.Content],
) {
def getSignature(): Either[IOException, Option[String]] = {
optContent
.map(_.map(content => MD5.bytesToHex(content)))
.map(Right[IOException, Option[String]](_))
.getOrElse {
try {
if (path.exists)
Right(Some(MD5.bytesToHex(FileIO.readAllBytes(path))))
else Right(None)
} catch {
case e: IOException =>
scribe.warn(s"Failed to read contents of $path", e)
Left(e)
}
}
}
}

object PathWithContent {
// None if the file doesn't exist
type Content = Option[Array[Byte]]
def apply(path: AbsolutePath) = new PathWithContent(path, None)
def apply(path: AbsolutePath, content: Array[Byte]) =
new PathWithContent(path, Some(Some(content)))
def deleted(path: AbsolutePath) = new PathWithContent(path, Some(None))
}
7 changes: 4 additions & 3 deletions tests/slow/src/test/scala/tests/sbt/CancelCompileSuite.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package tests

import scala.meta.internal.metals.MetalsServerConfig
import scala.meta.internal.metals.PathWithContent
import scala.meta.internal.metals.ServerCommands
import scala.meta.internal.metals.{BuildInfo => V}

Expand Down Expand Up @@ -58,17 +59,17 @@ class CancelCompileSuite
_ <- server.server.buildServerPromise.future
(compileReport, _) <- server.server.compilations
.compileFile(
workspace.resolve("c/src/main/scala/c/C.scala")
PathWithContent(workspace.resolve("c/src/main/scala/c/C.scala"))
)
.zip {
// wait until the compilation start
Thread.sleep(1000)
server.executeCommand(ServerCommands.CancelCompile)
}
_ = assertNoDiff(client.workspaceDiagnostics, "")
_ = assertEquals(compileReport.getStatusCode(), StatusCode.CANCELLED)
_ = assertEquals(compileReport.get.getStatusCode(), StatusCode.CANCELLED)
_ <- server.server.compilations.compileFile(
workspace.resolve("c/src/main/scala/c/C.scala")
PathWithContent(workspace.resolve("c/src/main/scala/c/C.scala"))
)
_ = assertNoDiff(
client.workspaceDiagnostics,
Expand Down
3 changes: 2 additions & 1 deletion tests/slow/src/test/scala/tests/sbt/SbtServerSuite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import scala.meta.internal.metals.CreateSession
import scala.meta.internal.metals.Messages
import scala.meta.internal.metals.Messages.ImportBuildChanges
import scala.meta.internal.metals.MetalsEnrichments._
import scala.meta.internal.metals.PathWithContent
import scala.meta.internal.metals.ServerCommands
import scala.meta.internal.metals.{BuildInfo => V}
import scala.meta.internal.process.SystemProcess
Expand Down Expand Up @@ -409,7 +410,7 @@ class SbtServerSuite
_ <- initialize(layout)
// make sure to compile once
_ <- server.server.compilations.compileFile(
workspace.resolve("target/Foo.scala")
PathWithContent(workspace.resolve("target/Foo.scala"))
)
} yield {
// Sleep 100 ms: that should be enough to see the compilation looping
Expand Down
9 changes: 7 additions & 2 deletions tests/unit/src/main/scala/tests/TestingServer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1057,7 +1057,10 @@ final case class TestingServer(

val compilations =
paths.map(path =>
fullServer.getServiceFor(path).compilations.compileFile(path)
fullServer
.getServiceFor(path)
.compilations
.compileFile(m.internal.metals.PathWithContent(path))
)

for {
Expand Down Expand Up @@ -1119,7 +1122,9 @@ final case class TestingServer(
codeLenses.trySuccess(lenses.toList)
else if (retries > 0) {
retries -= 1
server.compilations.compileFile(path)
server.compilations.compileFile(
m.internal.metals.PathWithContent(path)
)
} else {
val error = s"Could not fetch any code lenses in $maxRetries tries"
codeLenses.tryFailure(new NoSuchElementException(error))
Expand Down
9 changes: 5 additions & 4 deletions tests/unit/src/test/scala/tests/BillLspSuite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import scala.concurrent.Future
import scala.meta.internal.metals.Directories
import scala.meta.internal.metals.Messages
import scala.meta.internal.metals.MetalsEnrichments._
import scala.meta.internal.metals.PathWithContent
import scala.meta.internal.metals.RecursivelyDelete
import scala.meta.internal.metals.ServerCommands
import scala.meta.io.AbsolutePath
Expand Down Expand Up @@ -241,7 +242,7 @@ class BillLspSuite extends BaseLspSuite("bill") {
)
(compileReport, _) <- server.server.compilations
.compileFile(
workspace.resolve("src/com/App.scala")
PathWithContent(workspace.resolve("src/com/App.scala"))
)
.zip {
// wait until the compilation start
Expand All @@ -250,7 +251,7 @@ class BillLspSuite extends BaseLspSuite("bill") {
}
server.executeCommand(ServerCommands.CancelCompile)
}
_ = assertEquals(compileReport.getStatusCode(), StatusCode.CANCELLED)
_ = assertEquals(compileReport.get.getStatusCode(), StatusCode.CANCELLED)
// wait for all the side effect (`onComplete`) actions of cancellation to happen
_ = Thread.sleep(1000)
currentTrace = trace
Expand All @@ -260,9 +261,9 @@ class BillLspSuite extends BaseLspSuite("bill") {
_ = assert(currentTrace.contains(s"buildTarget/compile - ($cancelId)"))
compileReport <- server.server.compilations
.compileFile(
workspace.resolve("src/com/App.scala")
PathWithContent(workspace.resolve("src/com/App.scala"))
)
_ = assertEquals(compileReport.getStatusCode(), StatusCode.OK)
_ = assertEquals(compileReport.get.getStatusCode(), StatusCode.OK)
} yield ()
}
}
7 changes: 4 additions & 3 deletions tests/unit/src/test/scala/tests/CancelCompileLspSuite.scala
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package tests

import scala.meta.internal.metals.PathWithContent
import scala.meta.internal.metals.ServerCommands

import ch.epfl.scala.bsp4j.StatusCode
Expand Down Expand Up @@ -48,17 +49,17 @@ class CancelCompileLspSuite extends BaseLspSuite("compile-cancel") {
_ <- server.server.buildServerPromise.future
(compileReport, _) <- server.server.compilations
.compileFile(
workspace.resolve("c/src/main/scala/c/C.scala")
PathWithContent(workspace.resolve("c/src/main/scala/c/C.scala"))
)
.zip {
// wait until the compilation start
Thread.sleep(1000)
server.executeCommand(ServerCommands.CancelCompile)
}
_ = assertNoDiff(client.workspaceDiagnostics, "")
_ = assertEquals(compileReport.getStatusCode(), StatusCode.CANCELLED)
_ = assertEquals(compileReport.get.getStatusCode(), StatusCode.CANCELLED)
_ <- server.server.compilations.compileFile(
workspace.resolve("c/src/main/scala/c/C.scala")
PathWithContent(workspace.resolve("c/src/main/scala/c/C.scala"))
)
_ = assertNoDiff(
client.workspaceDiagnostics,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import scala.util.Success
import scala.meta.internal.metals.ClientCommands
import scala.meta.internal.metals.InitializationOptions
import scala.meta.internal.metals.MetalsEnrichments._
import scala.meta.internal.metals.PathWithContent

class UnsupportedDebuggingLspSuite
extends BaseLspSuite("unsupported-debugging") {
Expand Down Expand Up @@ -62,7 +63,9 @@ class UnsupportedDebuggingLspSuite
)
_ <-
server.server.compilations
.compileFile(server.toPath("a/src/main/scala/Main.scala"))
.compileFile(
PathWithContent(server.toPath("a/src/main/scala/Main.scala"))
)
} yield {
val clientCommands = client.clientCommands.asScala.map(_.getCommand).toSet
assert(!clientCommands.contains(ClientCommands.RefreshModel.id))
Expand Down
Loading