Skip to content

Commit

Permalink
Restart presentation compilers if memory is low
Browse files Browse the repository at this point in the history
I have noted during long editing sessions (lasting several days,
typically) that memory can get full because the Dotty compiler has some
space leaks. The leaks looks really hard to fix, and we don't know
yet whether it's at all possible. To mitigate the leaks, this commit
makes the language server watch available memory, and, if it
is low (i.e. free memory after a GC < 10% of maximal available)
restart all interactive drivers. This will free all memory of
the compiler(s) except the shared nametable.

There's a stressTest option in `Memory.scala`, which, when turned on,
causes a restart every 10 editing actions. I verified that the compiler
stays functional and reasonably responsive in that mode.
  • Loading branch information
odersky committed Feb 6, 2018
1 parent 0519aca commit 4419859
Show file tree
Hide file tree
Showing 4 changed files with 97 additions and 11 deletions.
4 changes: 3 additions & 1 deletion compiler/src/dotty/tools/dotc/Run.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
19 changes: 12 additions & 7 deletions compiler/src/dotty/tools/dotc/interactive/InteractiveDriver.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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._

Expand Down Expand Up @@ -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 =
Expand All @@ -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))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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 =
Expand Down Expand Up @@ -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 {
Expand All @@ -114,7 +144,7 @@ class DottyLanguageServer extends LanguageServer
throw ex
}
}
})
}

override def initialize(params: InitializeParams) = computeAsync { cancelToken =>
rootUri = params.getRootUri
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down
47 changes: 47 additions & 0 deletions language-server/src/dotty/tools/languageserver/Memory.scala
Original file line number Diff line number Diff line change
@@ -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"
}
}

0 comments on commit 4419859

Please sign in to comment.