Skip to content

Commit 8c3319c

Browse files
committed
Run worksheets asynchronously
A worksheet in an infinite loop shouldn't block the whole language server from responding to queries. We just need to be careful to lock the language server when operating on trees since these operations may not be thread-safe (for example, when a symbol gets forced). We could also allow running multiple worksheets at the same time but this commit doesn't implement that.
1 parent 99ac060 commit 8c3319c

File tree

5 files changed

+65
-61
lines changed

5 files changed

+65
-61
lines changed

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

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ class DottyLanguageServer extends LanguageServer
107107
if (Memory.isCritical()) CompletableFutures.computeAsync { _ => restart() }
108108

109109
/** The driver instance responsible for compiling `uri` */
110-
def driverFor(uri: URI): InteractiveDriver = {
110+
def driverFor(uri: URI): InteractiveDriver = thisServer.synchronized {
111111
val matchingConfig =
112112
drivers.keys.find(config => config.sourceDirectories.exists(sourceDir =>
113113
new File(uri.getPath).getCanonicalPath.startsWith(sourceDir.getCanonicalPath)))
@@ -133,10 +133,10 @@ class DottyLanguageServer extends LanguageServer
133133
CompletableFuture.completedFuture(new Object)
134134
}
135135

136-
def computeAsync[R](fun: CancelChecker => R): CompletableFuture[R] =
136+
def computeAsync[R](fun: CancelChecker => R, synchronize: Boolean = true): CompletableFuture[R] =
137137
CompletableFutures.computeAsync { cancelToken =>
138138
// We do not support any concurrent use of the compiler currently.
139-
thisServer.synchronized {
139+
def computation(): R = {
140140
cancelToken.checkCanceled()
141141
checkMemory()
142142
try {
@@ -147,6 +147,10 @@ class DottyLanguageServer extends LanguageServer
147147
throw ex
148148
}
149149
}
150+
if (synchronize)
151+
thisServer.synchronized { computation() }
152+
else
153+
computation()
150154
}
151155

152156
override def initialize(params: InitializeParams) = computeAsync { cancelToken =>

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ object Main {
7373
.setInput(in)
7474
.setOutput(out)
7575
// For debugging JSON messages:
76-
// .traceMessages(new java.io.PrintWriter(System.err, true))
76+
//.traceMessages(new java.io.PrintWriter(System.err, true))
7777
.create();
7878

7979
val client = launcher.getRemoteProxy()

language-server/src/dotty/tools/languageserver/worksheet/Evaluator.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ private object Evaluator {
2828
* @param cancelChecker The token that indicates whether evaluation has been cancelled.
2929
* @return A JVM running the REPL.
3030
*/
31-
def get(cancelChecker: CancelChecker)(implicit ctx: Context): Option[Evaluator] = {
31+
def get(cancelChecker: CancelChecker)(implicit ctx: Context): Option[Evaluator] = synchronized {
3232
val classpath = ctx.settings.classpath.value
3333
previousEvaluator match {
3434
case Some(cp, evaluator) if evaluator.isAlive() && cp == classpath =>

language-server/src/dotty/tools/languageserver/worksheet/Worksheet.scala

Lines changed: 35 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -10,60 +10,65 @@ import dotty.tools.dotc.core.Flags.Synthetic
1010

1111
import org.eclipse.lsp4j.jsonrpc.CancelChecker
1212

13-
import java.util.concurrent.CancellationException
14-
1513
object Worksheet {
1614

1715
/**
1816
* Run `tree` as a worksheet using the REPL.
1917
*
2018
* @param tree The top level object wrapping the worksheet.
19+
* @param treeLock Object on which to lock when doing operations on trees.
2120
* @param sendMessage A mean of communicating the results of evaluation back.
2221
* @param cancelChecker A token to check whether execution should be cancelled.
2322
*/
2423
def run(tree: SourceTree,
24+
treeLock: Object,
2525
sendMessage: (Int, String) => Unit,
2626
cancelChecker: CancelChecker)(
27-
implicit ctx: Context): Unit = synchronized {
28-
29-
Evaluator.get(cancelChecker) match {
30-
case None =>
31-
sendMessage(1, "Couldn't start JVM.")
32-
case Some(evaluator) =>
33-
tree.tree match {
34-
case td @ TypeDef(_, template: Template) =>
35-
val executed = collection.mutable.Set.empty[(Int, Int)]
36-
37-
template.body.foreach {
38-
case statement: DefTree if statement.symbol.is(Synthetic) =>
39-
()
27+
implicit ctx: Context): Unit = {
28+
// For now, don't try to run multiple evaluators in parallel, this would require
29+
// changes to the logic of Evaluator.get among other things.
30+
Evaluator.synchronized {
31+
Evaluator.get(cancelChecker) match {
32+
case None =>
33+
sendMessage(1, "Couldn't start the JVM.")
34+
case Some(evaluator) =>
35+
val queries = treeLock.synchronized {
36+
tree.tree match {
37+
case td @ TypeDef(_, template: Template) =>
38+
val seen = collection.mutable.Set.empty[(Int, Int)]
4039

41-
case statement if evaluator.isAlive() && executed.add(bounds(statement.pos)) =>
42-
try {
43-
cancelChecker.checkCanceled()
44-
val (line, result) = execute(evaluator, statement, tree.source)
45-
if (result.nonEmpty) sendMessage(line, result)
46-
} catch { case _: CancellationException => () }
47-
48-
case _ =>
49-
()
40+
template.body.flatMap {
41+
case statement: DefTree if statement.symbol.is(Synthetic) =>
42+
None
43+
case statement if seen.add(bounds(statement.pos)) =>
44+
Some(query(statement, tree.source))
45+
case _ =>
46+
None
47+
}
5048
}
51-
}
49+
}
50+
queries.foreach { (line, code) =>
51+
cancelChecker.checkCanceled()
52+
val res = evaluator.eval(code).getOrElse("")
53+
cancelChecker.checkCanceled()
54+
if (res.nonEmpty)
55+
sendMessage(line, res)
56+
}
57+
}
5258
}
5359
}
5460

5561
/**
56-
* Extract `tree` from the source and evaluate it in the REPL.
62+
* Extract the line number and source code corresponding to this tree
5763
*
5864
* @param evaluator The JVM that runs the REPL.
5965
* @param tree The compiled tree to evaluate.
6066
* @param sourcefile The sourcefile of the worksheet.
61-
* @return The line in the sourcefile that corresponds to `tree`, and the result.
6267
*/
63-
private def execute(evaluator: Evaluator, tree: Tree, sourcefile: SourceFile): (Int, String) = {
64-
val source = sourcefile.content.slice(tree.pos.start, tree.pos.end).mkString
68+
private def query(tree: Tree, sourcefile: SourceFile): (Int, String) = {
6569
val line = sourcefile.offsetToLine(tree.pos.end)
66-
(line, evaluator.eval(source).getOrElse(""))
70+
val source = sourcefile.content.slice(tree.pos.start, tree.pos.end).mkString
71+
(line, source)
6772
}
6873

6974
private def bounds(pos: Position): (Int, Int) = (pos.start, pos.end)

language-server/src/dotty/tools/languageserver/worksheet/WorksheetService.scala

Lines changed: 21 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -13,28 +13,23 @@ import java.util.concurrent.{CompletableFuture, ConcurrentHashMap}
1313
@JsonSegment("worksheet")
1414
trait WorksheetService { thisServer: DottyLanguageServer =>
1515

16-
val worksheets: ConcurrentHashMap[URI, CompletableFuture[_]] = new ConcurrentHashMap()
17-
1816
@JsonRequest
19-
def run(params: WorksheetRunParams): CompletableFuture[WorksheetRunResult] = thisServer.synchronized {
20-
val uri = new URI(params.textDocument.getUri)
21-
val future =
22-
computeAsync { cancelChecker =>
23-
try {
24-
val driver = driverFor(uri)
25-
val sendMessage = (line: Int, msg: String) => client.publishOutput(WorksheetRunOutput(params.textDocument, line, msg))
26-
runWorksheet(driver, uri, sendMessage, cancelChecker)(driver.currentCtx)
27-
WorksheetRunResult(success = true)
28-
} catch {
29-
case _: Throwable =>
30-
WorksheetRunResult(success = false)
31-
} finally {
32-
worksheets.remove(uri)
33-
}
17+
def run(params: WorksheetRunParams): CompletableFuture[WorksheetRunResult] =
18+
computeAsync(synchronize = false, fun = { cancelChecker =>
19+
val uri = new URI(params.textDocument.getUri)
20+
try {
21+
val driver = driverFor(uri)
22+
val sendMessage =
23+
(line: Int, msg: String) => client.publishOutput(WorksheetRunOutput(params.textDocument, line, msg))
24+
25+
runWorksheet(driver, uri, sendMessage, cancelChecker)(driver.currentCtx)
26+
cancelChecker.checkCanceled()
27+
WorksheetRunResult(success = true)
28+
} catch {
29+
case _: Throwable =>
30+
WorksheetRunResult(success = false)
3431
}
35-
worksheets.put(uri, future)
36-
future
37-
}
32+
})
3833

3934
/**
4035
* Run the worksheet at `uri`.
@@ -45,13 +40,13 @@ trait WorksheetService { thisServer: DottyLanguageServer =>
4540
* @param cancelChecker Token to check whether evaluation was cancelled
4641
*/
4742
private def runWorksheet(driver: InteractiveDriver,
48-
uri: URI,
49-
sendMessage: (Int, String) => Unit,
50-
cancelChecker: CancelChecker)(
43+
uri: URI,
44+
sendMessage: (Int, String) => Unit,
45+
cancelChecker: CancelChecker)(
5146
implicit ctx: Context): Unit = {
52-
val trees = driver.openedTrees(uri)
53-
trees.headOption.foreach { tree =>
54-
Worksheet.run(tree, sendMessage, cancelChecker)
47+
val treeOpt = thisServer.synchronized {
48+
driver.openedTrees(uri).headOption
5549
}
50+
treeOpt.foreach(tree => Worksheet.run(tree, thisServer, sendMessage, cancelChecker))
5651
}
5752
}

0 commit comments

Comments
 (0)