Skip to content

Commit 14c5af6

Browse files
authored
Merge pull request #3967 from dotty-staging/ide-restart
Restart presentation compilers if memory is low
2 parents 6ac9bf2 + 659ab0b commit 14c5af6

File tree

6 files changed

+91
-17
lines changed

6 files changed

+91
-17
lines changed

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

+3-1
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/core/Types.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -2184,7 +2184,7 @@ object Types {
21842184
if (ctx.erasedTypes) tref
21852185
else cls.info match {
21862186
case cinfo: ClassInfo => cinfo.selfType
2187-
case cinfo: ErrorType if ctx.mode.is(Mode.Interactive) => cinfo
2187+
case _: ErrorType | NoType if ctx.mode.is(Mode.Interactive) => cls.info
21882188
// can happen in IDE if `cls` is stale
21892189
}
21902190

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

+4-5
Original file line numberDiff line numberDiff line change
@@ -341,11 +341,10 @@ object Interactive {
341341
}
342342

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

351350
def contextOfStat(stats: List[Tree], stat: Tree, exprOwner: Symbol, ctx: Context): Context = stats match {

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

+12-7
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

@@ -216,7 +216,17 @@ class InteractiveDriver(settings: List[String]) extends Driver {
216216
cleanupTree(tree)
217217
}
218218

219-
def run(uri: URI, sourceCode: String): List[MessageContainer] = {
219+
private def toSource(uri: URI, sourceCode: String): SourceFile = {
220+
val virtualFile = new VirtualFile(uri.toString, Paths.get(uri).toString)
221+
val writer = new BufferedWriter(new OutputStreamWriter(virtualFile.output, "UTF-8"))
222+
writer.write(sourceCode)
223+
writer.close()
224+
new SourceFile(virtualFile, Codec.UTF8)
225+
}
226+
227+
def run(uri: URI, sourceCode: String): List[MessageContainer] = run(uri, toSource(uri, sourceCode))
228+
229+
def run(uri: URI, source: SourceFile): List[MessageContainer] = {
220230
val previousCtx = myCtx
221231
try {
222232
val reporter =
@@ -227,11 +237,6 @@ class InteractiveDriver(settings: List[String]) extends Driver {
227237

228238
implicit val ctx = myCtx
229239

230-
val virtualFile = new VirtualFile(uri.toString, Paths.get(uri).toString)
231-
val writer = new BufferedWriter(new OutputStreamWriter(virtualFile.output, "UTF-8"))
232-
writer.write(sourceCode)
233-
writer.close()
234-
val source = new SourceFile(virtualFile, Codec.UTF8)
235240
myOpenedFiles(uri) = source
236241

237242
run.compileSources(List(source))

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

+24-3
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

@@ -78,12 +79,29 @@ class DottyLanguageServer extends LanguageServer
7879
.update("-classpath", (config.classDirectory +: config.dependencyClasspath).mkString(File.pathSeparator))
7980
.update("-sourcepath", config.sourceDirectories.mkString(File.pathSeparator)) :+
8081
"-scansource"
81-
myDrivers.put(config, new InteractiveDriver(settings))
82+
myDrivers(config) = new InteractiveDriver(settings)
8283
}
8384
}
8485
myDrivers
8586
}
8687

88+
/** Restart all presentation compiler drivers, copying open files over */
89+
private def restart() = thisServer.synchronized {
90+
interactiv.println("restarting presentation compiler")
91+
val driverConfigs = for ((config, driver) <- myDrivers.toList) yield
92+
(config, new InteractiveDriver(driver.settings), driver.openedFiles)
93+
for ((config, driver, _) <- driverConfigs)
94+
myDrivers(config) = driver
95+
System.gc()
96+
for ((_, driver, opened) <- driverConfigs; (uri, source) <- opened)
97+
driver.run(uri, source)
98+
if (Memory.isCritical())
99+
println(s"WARNING: Insufficient memory to run Scala language server on these projects.")
100+
}
101+
102+
private def checkMemory() =
103+
if (Memory.isCritical()) CompletableFutures.computeAsync { _ => restart() }
104+
87105
/** The driver instance responsible for compiling `uri` */
88106
def driverFor(uri: URI): InteractiveDriver = {
89107
val matchingConfig =
@@ -112,10 +130,11 @@ class DottyLanguageServer extends LanguageServer
112130
}
113131

114132
private[this] def computeAsync[R](fun: CancelChecker => R): CompletableFuture[R] =
115-
CompletableFutures.computeAsync({(cancelToken: CancelChecker) =>
133+
CompletableFutures.computeAsync { cancelToken =>
116134
// We do not support any concurrent use of the compiler currently.
117135
thisServer.synchronized {
118136
cancelToken.checkCanceled()
137+
checkMemory()
119138
try {
120139
fun(cancelToken)
121140
} catch {
@@ -124,7 +143,7 @@ class DottyLanguageServer extends LanguageServer
124143
throw ex
125144
}
126145
}
127-
})
146+
}
128147

129148
override def initialize(params: InitializeParams) = computeAsync { cancelToken =>
130149
rootUri = params.getRootUri
@@ -160,6 +179,7 @@ class DottyLanguageServer extends LanguageServer
160179
}
161180

162181
override def didOpen(params: DidOpenTextDocumentParams): Unit = thisServer.synchronized {
182+
checkMemory()
163183
val document = params.getTextDocument
164184
val uri = new URI(document.getUri)
165185
val driver = driverFor(uri)
@@ -173,6 +193,7 @@ class DottyLanguageServer extends LanguageServer
173193
}
174194

175195
override def didChange(params: DidChangeTextDocumentParams): Unit = thisServer.synchronized {
196+
checkMemory()
176197
val document = params.getTextDocument
177198
val uri = new URI(document.getUri)
178199
val driver = driverFor(uri)
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)