Skip to content

Commit 4419859

Browse files
committed
Restart presentation compilers if memory is low
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.
1 parent 0519aca commit 4419859

File tree

4 files changed

+97
-11
lines changed

4 files changed

+97
-11
lines changed

compiler/src/dotty/tools/dotc/Run.scala

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,9 @@ class Run(comp: Compiler, ictx: Context) extends ImplicitRunInfo with Constraint
147147
}
148148

149149
protected def compileUnits()(implicit ctx: Context) = Stats.maybeMonitored {
150-
ctx.checkSingleThreaded()
150+
if (!ctx.mode.is(Mode.Interactive)) // IDEs might have multi-threaded access, accesses are synchronized
151+
ctx.checkSingleThreaded()
152+
151153
compiling = true
152154

153155
// If testing pickler, make sure to stop after pickling phase:

compiler/src/dotty/tools/dotc/interactive/InteractiveDriver.scala

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import reporting._, reporting.diagnostic.MessageContainer
2424
import util._
2525

2626
/** A Driver subclass designed to be used from IDEs */
27-
class InteractiveDriver(settings: List[String]) extends Driver {
27+
class InteractiveDriver(val settings: List[String]) extends Driver {
2828
import tpd._
2929
import InteractiveDriver._
3030

@@ -212,7 +212,17 @@ class InteractiveDriver(settings: List[String]) extends Driver {
212212
cleanupTree(tree)
213213
}
214214

215-
def run(uri: URI, sourceCode: String): List[MessageContainer] = {
215+
private def toSource(uri: URI, sourceCode: String): SourceFile = {
216+
val virtualFile = new VirtualFile(uri.toString, Paths.get(uri).toString)
217+
val writer = new BufferedWriter(new OutputStreamWriter(virtualFile.output, "UTF-8"))
218+
writer.write(sourceCode)
219+
writer.close()
220+
new SourceFile(virtualFile, Codec.UTF8)
221+
}
222+
223+
def run(uri: URI, sourceCode: String): List[MessageContainer] = run(uri, toSource(uri, sourceCode))
224+
225+
def run(uri: URI, source: SourceFile): List[MessageContainer] = {
216226
val previousCtx = myCtx
217227
try {
218228
val reporter =
@@ -223,11 +233,6 @@ class InteractiveDriver(settings: List[String]) extends Driver {
223233

224234
implicit val ctx = myCtx
225235

226-
val virtualFile = new VirtualFile(uri.toString, Paths.get(uri).toString)
227-
val writer = new BufferedWriter(new OutputStreamWriter(virtualFile.output, "UTF-8"))
228-
writer.write(sourceCode)
229-
writer.close()
230-
val source = new SourceFile(virtualFile, Codec.UTF8)
231236
myOpenedFiles(uri) = source
232237

233238
run.compileSources(List(source))

language-server/src/dotty/tools/languageserver/DottyLanguageServer.scala

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import reporting._, reporting.diagnostic.MessageContainer
2525
import util._
2626
import interactive._, interactive.InteractiveDriver._
2727
import Interactive.Include
28+
import config.Printers.interactiv
2829

2930
import languageserver.config.ProjectConfig
3031

@@ -68,12 +69,40 @@ class DottyLanguageServer extends LanguageServer
6869
val classpathFlags = List("-classpath", (config.classDirectory +: config.dependencyClasspath).mkString(File.pathSeparator))
6970
val sourcepathFlags = List("-sourcepath", config.sourceDirectories.mkString(File.pathSeparator), "-scansource")
7071
val settings = defaultFlags ++ config.compilerArguments.toList ++ classpathFlags ++ sourcepathFlags
71-
myDrivers.put(config, new InteractiveDriver(settings))
72+
myDrivers(config) = new InteractiveDriver(settings)
7273
}
7374
}
7475
myDrivers
7576
}
7677

78+
/** Restart all presentation compiler drivers, copying open files over */
79+
private def restart() = thisServer.synchronized {
80+
interactiv.println("restarting presentation compiler")
81+
val driverConfigs = for ((config, driver) <- myDrivers.toList) yield
82+
(config, new InteractiveDriver(driver.settings), driver.openedFiles)
83+
for ((config, driver, _) <- driverConfigs)
84+
myDrivers(config) = driver
85+
System.gc()
86+
for ((_, driver, opened) <- driverConfigs; (uri, source) <- opened)
87+
driver.run(uri, source)
88+
if (Memory.isCritical())
89+
println(s"WARNING: Insufficient memory to run Scala language server on these projects.")
90+
}
91+
92+
private def checkMemory() =
93+
if (Memory.isCritical())
94+
CompletableFutures.computeAsync { _ => restart(); new Object() }
95+
// new Object() necessary or we get a BootstrapMethodError:
96+
//
97+
// Caused by: java.lang.invoke.LambdaConversionException: Type mismatch for lambda expected return: void is not convertible to class java.lang.Object
98+
// at java.lang.invoke.AbstractValidatingLambdaMetafactory.validateMetafactoryArgs(AbstractValidatingLambdaMetafactory.java:286)
99+
// at java.lang.invoke.LambdaMetafactory.metafactory(LambdaMetafactory.java:303)
100+
// at java.lang.invoke.CallSite.makeSite(CallSite.java:302)
101+
// ... 11 more
102+
//
103+
// This looks like a problem with Dottys code generation for void-returning closures passed
104+
// to Java methods. (or SAM functions in general?)
105+
77106
/** The driver instance responsible for compiling `uri` */
78107
def driverFor(uri: URI): InteractiveDriver = {
79108
val matchingConfig =
@@ -102,10 +131,11 @@ class DottyLanguageServer extends LanguageServer
102131
}
103132

104133
private[this] def computeAsync[R](fun: CancelChecker => R): CompletableFuture[R] =
105-
CompletableFutures.computeAsync({(cancelToken: CancelChecker) =>
134+
CompletableFutures.computeAsync { cancelToken =>
106135
// We do not support any concurrent use of the compiler currently.
107136
thisServer.synchronized {
108137
cancelToken.checkCanceled()
138+
checkMemory()
109139
try {
110140
fun(cancelToken)
111141
} catch {
@@ -114,7 +144,7 @@ class DottyLanguageServer extends LanguageServer
114144
throw ex
115145
}
116146
}
117-
})
147+
}
118148

119149
override def initialize(params: InitializeParams) = computeAsync { cancelToken =>
120150
rootUri = params.getRootUri
@@ -150,6 +180,7 @@ class DottyLanguageServer extends LanguageServer
150180
}
151181

152182
override def didOpen(params: DidOpenTextDocumentParams): Unit = thisServer.synchronized {
183+
checkMemory()
153184
val document = params.getTextDocument
154185
val uri = new URI(document.getUri)
155186
val driver = driverFor(uri)
@@ -163,6 +194,7 @@ class DottyLanguageServer extends LanguageServer
163194
}
164195

165196
override def didChange(params: DidChangeTextDocumentParams): Unit = thisServer.synchronized {
197+
checkMemory()
166198
val document = params.getTextDocument
167199
val uri = new URI(document.getUri)
168200
val driver = driverFor(uri)
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package dotty.tools
2+
package languageserver
3+
4+
object Memory {
5+
6+
/** Memory is judged to be critical if after a GC the amount of used memory
7+
* divided by total available memory exceeds this threshold.
8+
*/
9+
val UsedThreshold = 0.9
10+
11+
/** If total available memory is unknown, memory is judged to be critical if
12+
* after a GC free memory divided by used memory is under this threshold.
13+
*/
14+
val FreeThreshold = 0.1
15+
16+
/** Turn this flag on to stress test restart capability in compiler.
17+
* It will restart the presentation compiler after every 10 editing actions
18+
*/
19+
private final val stressTest = false
20+
private var stressTestCounter = 0
21+
22+
/** Is memory critically low? */
23+
def isCritical(): Boolean = {
24+
if (stressTest) {
25+
stressTestCounter += 1
26+
if (stressTestCounter % 10 == 0) return true
27+
}
28+
val runtime = Runtime.getRuntime
29+
def total = runtime.totalMemory
30+
def maximal = runtime.maxMemory
31+
def free = runtime.freeMemory
32+
def used = total - free
33+
def usedIsCloseToMax =
34+
if (maximal == Long.MaxValue) free.toDouble / used < FreeThreshold
35+
else used.toDouble / maximal > UsedThreshold
36+
usedIsCloseToMax && { runtime.gc(); usedIsCloseToMax }
37+
}
38+
39+
def stats(): String = {
40+
final val M = 2 << 20
41+
val runtime = Runtime.getRuntime
42+
def total = runtime.totalMemory / M
43+
def maximal = runtime.maxMemory / M
44+
def free = runtime.freeMemory / M
45+
s"total used memory: $total MB, free: $free MB, maximal available = $maximal MB"
46+
}
47+
}

0 commit comments

Comments
 (0)