diff --git a/compiler/src/dotty/tools/dotc/Run.scala b/compiler/src/dotty/tools/dotc/Run.scala index 925890d4ff4c..b6c401f55f22 100644 --- a/compiler/src/dotty/tools/dotc/Run.scala +++ b/compiler/src/dotty/tools/dotc/Run.scala @@ -147,7 +147,9 @@ class Run(comp: Compiler, ictx: Context) extends ImplicitRunInfo with Constraint } protected def compileUnits()(implicit ctx: Context) = Stats.maybeMonitored { - ctx.checkSingleThreaded() + if (!ctx.mode.is(Mode.Interactive)) // IDEs might have multi-threaded access, accesses are synchronized + ctx.checkSingleThreaded() + compiling = true // If testing pickler, make sure to stop after pickling phase: diff --git a/compiler/src/dotty/tools/dotc/interactive/InteractiveDriver.scala b/compiler/src/dotty/tools/dotc/interactive/InteractiveDriver.scala index 3940547ffa81..dd7c682c9c7e 100644 --- a/compiler/src/dotty/tools/dotc/interactive/InteractiveDriver.scala +++ b/compiler/src/dotty/tools/dotc/interactive/InteractiveDriver.scala @@ -24,7 +24,7 @@ import reporting._, reporting.diagnostic.MessageContainer import util._ /** A Driver subclass designed to be used from IDEs */ -class InteractiveDriver(settings: List[String]) extends Driver { +class InteractiveDriver(val settings: List[String]) extends Driver { import tpd._ import InteractiveDriver._ @@ -212,7 +212,17 @@ class InteractiveDriver(settings: List[String]) extends Driver { cleanupTree(tree) } - def run(uri: URI, sourceCode: String): List[MessageContainer] = { + private def toSource(uri: URI, sourceCode: String): SourceFile = { + val virtualFile = new VirtualFile(uri.toString, Paths.get(uri).toString) + val writer = new BufferedWriter(new OutputStreamWriter(virtualFile.output, "UTF-8")) + writer.write(sourceCode) + writer.close() + new SourceFile(virtualFile, Codec.UTF8) + } + + def run(uri: URI, sourceCode: String): List[MessageContainer] = run(uri, toSource(uri, sourceCode)) + + def run(uri: URI, source: SourceFile): List[MessageContainer] = { val previousCtx = myCtx try { val reporter = @@ -223,11 +233,6 @@ class InteractiveDriver(settings: List[String]) extends Driver { implicit val ctx = myCtx - val virtualFile = new VirtualFile(uri.toString, Paths.get(uri).toString) - val writer = new BufferedWriter(new OutputStreamWriter(virtualFile.output, "UTF-8")) - writer.write(sourceCode) - writer.close() - val source = new SourceFile(virtualFile, Codec.UTF8) myOpenedFiles(uri) = source run.compileSources(List(source)) diff --git a/language-server/src/dotty/tools/languageserver/DottyLanguageServer.scala b/language-server/src/dotty/tools/languageserver/DottyLanguageServer.scala index 67cc15452855..12e8c4220780 100644 --- a/language-server/src/dotty/tools/languageserver/DottyLanguageServer.scala +++ b/language-server/src/dotty/tools/languageserver/DottyLanguageServer.scala @@ -25,6 +25,7 @@ import reporting._, reporting.diagnostic.MessageContainer import util._ import interactive._, interactive.InteractiveDriver._ import Interactive.Include +import config.Printers.interactiv import languageserver.config.ProjectConfig @@ -68,12 +69,40 @@ class DottyLanguageServer extends LanguageServer val classpathFlags = List("-classpath", (config.classDirectory +: config.dependencyClasspath).mkString(File.pathSeparator)) val sourcepathFlags = List("-sourcepath", config.sourceDirectories.mkString(File.pathSeparator), "-scansource") val settings = defaultFlags ++ config.compilerArguments.toList ++ classpathFlags ++ sourcepathFlags - myDrivers.put(config, new InteractiveDriver(settings)) + myDrivers(config) = new InteractiveDriver(settings) } } myDrivers } + /** Restart all presentation compiler drivers, copying open files over */ + private def restart() = thisServer.synchronized { + interactiv.println("restarting presentation compiler") + val driverConfigs = for ((config, driver) <- myDrivers.toList) yield + (config, new InteractiveDriver(driver.settings), driver.openedFiles) + for ((config, driver, _) <- driverConfigs) + myDrivers(config) = driver + System.gc() + for ((_, driver, opened) <- driverConfigs; (uri, source) <- opened) + driver.run(uri, source) + if (Memory.isCritical()) + println(s"WARNING: Insufficient memory to run Scala language server on these projects.") + } + + private def checkMemory() = + if (Memory.isCritical()) + CompletableFutures.computeAsync { _ => restart(); new Object() } + // new Object() necessary or we get a BootstrapMethodError: + // + // Caused by: java.lang.invoke.LambdaConversionException: Type mismatch for lambda expected return: void is not convertible to class java.lang.Object + // at java.lang.invoke.AbstractValidatingLambdaMetafactory.validateMetafactoryArgs(AbstractValidatingLambdaMetafactory.java:286) + // at java.lang.invoke.LambdaMetafactory.metafactory(LambdaMetafactory.java:303) + // at java.lang.invoke.CallSite.makeSite(CallSite.java:302) + // ... 11 more + // + // This looks like a problem with Dottys code generation for void-returning closures passed + // to Java methods. (or SAM functions in general?) + /** The driver instance responsible for compiling `uri` */ def driverFor(uri: URI): InteractiveDriver = { val matchingConfig = @@ -102,10 +131,11 @@ class DottyLanguageServer extends LanguageServer } private[this] def computeAsync[R](fun: CancelChecker => R): CompletableFuture[R] = - CompletableFutures.computeAsync({(cancelToken: CancelChecker) => + CompletableFutures.computeAsync { cancelToken => // We do not support any concurrent use of the compiler currently. thisServer.synchronized { cancelToken.checkCanceled() + checkMemory() try { fun(cancelToken) } catch { @@ -114,7 +144,7 @@ class DottyLanguageServer extends LanguageServer throw ex } } - }) + } override def initialize(params: InitializeParams) = computeAsync { cancelToken => rootUri = params.getRootUri @@ -150,6 +180,7 @@ class DottyLanguageServer extends LanguageServer } override def didOpen(params: DidOpenTextDocumentParams): Unit = thisServer.synchronized { + checkMemory() val document = params.getTextDocument val uri = new URI(document.getUri) val driver = driverFor(uri) @@ -163,6 +194,7 @@ class DottyLanguageServer extends LanguageServer } override def didChange(params: DidChangeTextDocumentParams): Unit = thisServer.synchronized { + checkMemory() val document = params.getTextDocument val uri = new URI(document.getUri) val driver = driverFor(uri) diff --git a/language-server/src/dotty/tools/languageserver/Memory.scala b/language-server/src/dotty/tools/languageserver/Memory.scala new file mode 100644 index 000000000000..7193df0732ea --- /dev/null +++ b/language-server/src/dotty/tools/languageserver/Memory.scala @@ -0,0 +1,47 @@ +package dotty.tools +package languageserver + +object Memory { + + /** Memory is judged to be critical if after a GC the amount of used memory + * divided by total available memory exceeds this threshold. + */ + val UsedThreshold = 0.9 + + /** If total available memory is unknown, memory is judged to be critical if + * after a GC free memory divided by used memory is under this threshold. + */ + val FreeThreshold = 0.1 + + /** Turn this flag on to stress test restart capability in compiler. + * It will restart the presentation compiler after every 10 editing actions + */ + private final val stressTest = false + private var stressTestCounter = 0 + + /** Is memory critically low? */ + def isCritical(): Boolean = { + if (stressTest) { + stressTestCounter += 1 + if (stressTestCounter % 10 == 0) return true + } + val runtime = Runtime.getRuntime + def total = runtime.totalMemory + def maximal = runtime.maxMemory + def free = runtime.freeMemory + def used = total - free + def usedIsCloseToMax = + if (maximal == Long.MaxValue) free.toDouble / used < FreeThreshold + else used.toDouble / maximal > UsedThreshold + usedIsCloseToMax && { runtime.gc(); usedIsCloseToMax } + } + + def stats(): String = { + final val M = 2 << 20 + val runtime = Runtime.getRuntime + def total = runtime.totalMemory / M + def maximal = runtime.maxMemory / M + def free = runtime.freeMemory / M + s"total used memory: $total MB, free: $free MB, maximal available = $maximal MB" + } +} \ No newline at end of file