Skip to content

Restart presentation compilers if memory is low #3967

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

Merged
merged 4 commits into from
Mar 15, 2018
Merged
Show file tree
Hide file tree
Changes from all 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
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
2 changes: 1 addition & 1 deletion compiler/src/dotty/tools/dotc/core/Types.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2184,7 +2184,7 @@ object Types {
if (ctx.erasedTypes) tref
else cls.info match {
case cinfo: ClassInfo => cinfo.selfType
case cinfo: ErrorType if ctx.mode.is(Mode.Interactive) => cinfo
case _: ErrorType | NoType if ctx.mode.is(Mode.Interactive) => cls.info
// can happen in IDE if `cls` is stale
}

Expand Down
9 changes: 4 additions & 5 deletions compiler/src/dotty/tools/dotc/interactive/Interactive.scala
Original file line number Diff line number Diff line change
Expand Up @@ -341,11 +341,10 @@ object Interactive {
}

def pathTo(tree: Tree, pos: Position)(implicit ctx: Context): List[Tree] =
if (tree.pos.contains(pos)) {
// FIXME: We shouldn't need a cast. Change NavigateAST.pathTo to return a List of Tree?
val path = NavigateAST.pathTo(pos, tree, skipZeroExtent = true).asInstanceOf[List[untpd.Tree]]
path.dropWhile(!_.hasType) collect { case t: tpd.Tree @unchecked => t }
}
if (tree.pos.contains(pos))
NavigateAST.pathTo(pos, tree, skipZeroExtent = true)
.collect { case t: untpd.Tree => t }
.dropWhile(!_.hasType).asInstanceOf[List[tpd.Tree]]
else Nil

def contextOfStat(stats: List[Tree], stat: Tree, exprOwner: Symbol, ctx: Context): Context = stats match {
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 @@ -216,7 +216,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 @@ -227,11 +237,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 @@ -78,12 +79,29 @@ class DottyLanguageServer extends LanguageServer
.update("-classpath", (config.classDirectory +: config.dependencyClasspath).mkString(File.pathSeparator))
.update("-sourcepath", config.sourceDirectories.mkString(File.pathSeparator)) :+
"-scansource"
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() }

/** The driver instance responsible for compiling `uri` */
def driverFor(uri: URI): InteractiveDriver = {
val matchingConfig =
Expand Down Expand Up @@ -112,10 +130,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 @@ -124,7 +143,7 @@ class DottyLanguageServer extends LanguageServer
throw ex
}
}
})
}

override def initialize(params: InitializeParams) = computeAsync { cancelToken =>
rootUri = params.getRootUri
Expand Down Expand Up @@ -160,6 +179,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 @@ -173,6 +193,7 @@ class DottyLanguageServer extends LanguageServer
}

override def didChange(params: DidChangeTextDocumentParams): Unit = thisServer.synchronized {
checkMemory()
Copy link
Contributor

Choose a reason for hiding this comment

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

Shouldn't checkMemory() be called also when doing the other operations? I imagine that completion and references in particular can require quite some memory.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Since they do not involve new runs, I don't think it's a problem. The leak comes from the memory of previous runs not being completely reclaimed.

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"
}
}