Skip to content

Commit b61613b

Browse files
committed
Keep evaluator alive between worksheet runs
This allows to shave a few seconds of every worksheet run, because starting up a new JVM is slow.
1 parent 3866ad3 commit b61613b

File tree

1 file changed

+180
-103
lines changed

1 file changed

+180
-103
lines changed

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

Lines changed: 180 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -8,24 +8,13 @@ import dotty.tools.dotc.util.SourceFile
88

99
import dotty.tools.dotc.core.Flags.Synthetic
1010

11-
import dotty.tools.repl.{ReplDriver, State}
12-
1311
import org.eclipse.lsp4j.jsonrpc.CancelChecker
1412

1513
import java.io.{File, InputStream, InputStreamReader, PrintStream}
1614
import java.util.concurrent.CancellationException
1715

1816
object Worksheet {
1917

20-
private val javaExec: Option[String] = {
21-
val isWindows = sys.props("os.name").toLowerCase().indexOf("win") >= 0
22-
val bin = new File(sys.props("java.home"), "bin")
23-
val java = new File(bin, if (isWindows) "java.exe" else "java")
24-
25-
if (java.exists()) Some(java.getAbsolutePath())
26-
else None
27-
}
28-
2918
/**
3019
* Evaluate `tree` as a worksheet using the REPL.
3120
*
@@ -36,132 +25,220 @@ object Worksheet {
3625
def evaluate(tree: SourceTree,
3726
sendMessage: String => Unit,
3827
cancelChecker: CancelChecker)(
39-
implicit ctx: Context): Unit = {
40-
41-
val replMain = dotty.tools.repl.WorksheetMain.getClass.getName.init
42-
val classpath = sys.props("java.class.path")
43-
val options = Array(javaExec.get, "-classpath", classpath, replMain) ++ replOptions
44-
val replProcess = new ProcessBuilder(options: _*).redirectErrorStream(true).start()
45-
46-
// The stream that we use to send commands to the REPL
47-
val replIn = new PrintStream(replProcess.getOutputStream())
48-
49-
// Messages coming out of the REPL
50-
val replOut = new ReplReader(replProcess.getInputStream())
51-
replOut.start()
52-
53-
// The thread that monitors cancellation
54-
val cancellationThread = new CancellationThread(replOut, replProcess, cancelChecker)
55-
cancellationThread.start()
56-
57-
// Wait for the REPL to be ready
58-
replOut.next()
59-
60-
tree.tree match {
61-
case td @ TypeDef(_, template: Template) =>
62-
val executed = collection.mutable.Set.empty[(Int, Int)]
63-
64-
template.body.foreach {
65-
case statement: DefTree if statement.symbol.is(Synthetic) =>
66-
()
67-
68-
case statement if executed.add(bounds(statement.pos)) =>
69-
val line = execute(replIn, statement, tree.source)
70-
val result = replOut.next().trim
71-
if (result.nonEmpty) sendMessage(encode(result, line))
72-
73-
case _ =>
74-
()
28+
implicit ctx: Context): Unit = synchronized {
29+
30+
Evaluator.get(cancelChecker) match {
31+
case None =>
32+
sendMessage(encode("Couldn't start JVM.", 1))
33+
case Some(evaluator) =>
34+
tree.tree match {
35+
case td @ TypeDef(_, template: Template) =>
36+
val executed = collection.mutable.Set.empty[(Int, Int)]
37+
38+
template.body.foreach {
39+
case statement: DefTree if statement.symbol.is(Synthetic) =>
40+
()
41+
42+
case statement if executed.add(bounds(statement.pos)) =>
43+
try {
44+
cancelChecker.checkCanceled()
45+
val (line, result) = execute(evaluator, statement, tree.source)
46+
if (result.nonEmpty) sendMessage(encode(result, line))
47+
} catch { case _: CancellationException => () }
48+
49+
case _ =>
50+
()
51+
}
7552
}
7653
}
7754
}
7855

7956
/**
8057
* Extract `tree` from the source and evaluate it in the REPL.
8158
*
82-
* @param replIn A stream to send commands to the REPL.
59+
* @param evaluator The JVM that runs the REPL.
8360
* @param tree The compiled tree to evaluate.
8461
* @param sourcefile The sourcefile of the worksheet.
85-
* @return The line in the sourcefile that corresponds to `tree`.
62+
* @return The line in the sourcefile that corresponds to `tree`, and the result.
8663
*/
87-
private def execute(replIn: PrintStream, tree: Tree, sourcefile: SourceFile): Int = {
64+
private def execute(evaluator: Evaluator, tree: Tree, sourcefile: SourceFile): (Int, String) = {
8865
val source = sourcefile.content.slice(tree.pos.start, tree.pos.end).mkString
8966
val line = sourcefile.offsetToLine(tree.pos.end)
90-
replIn.println(source)
91-
replIn.flush()
92-
line
67+
(line, evaluator.eval(source))
9368
}
9469

95-
private def replOptions(implicit ctx: Context): Array[String] =
96-
Array("-color:never", "-classpath", ctx.settings.classpath.value)
97-
9870
private def encode(message: String, line: Int): String =
9971
line + ":" + message
10072

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

75+
}
76+
77+
private object Evaluator {
78+
79+
private val javaExec: Option[String] = {
80+
val isWindows = sys.props("os.name").toLowerCase().indexOf("win") >= 0
81+
val bin = new File(sys.props("java.home"), "bin")
82+
val java = new File(bin, if (isWindows) "java.exe" else "java")
83+
84+
if (java.exists()) Some(java.getAbsolutePath())
85+
else None
86+
}
87+
88+
/**
89+
* The most recent Evaluator that was used. It can be reused if the user classpath hasn't changed
90+
* between two calls.
91+
*/
92+
private[this] var previousEvaluator: Option[(String, Evaluator)] = None
93+
10394
/**
104-
* Regularly check whether execution has been cancelled, kill REPL if it is.
95+
* Get a (possibly reused) Evaluator and set cancel checker.
10596
*
106-
* @param replReader The ReplReader that reads the output of the REPL
107-
* @param process The forked JVM that runs the REPL.
108-
* @param cancelChecker The token that reports cancellation.
97+
* @param cancelChecker The token that indicates whether evaluation has been cancelled.
98+
* @return A JVM running the REPL.
10999
*/
110-
private class CancellationThread(replReader: ReplReader, process: Process, cancelChecker: CancelChecker) extends Thread {
111-
private final val checkCancelledDelayMs = 50
112-
113-
override def run(): Unit = {
114-
try {
115-
while (!Thread.interrupted()) {
116-
cancelChecker.checkCanceled()
117-
Thread.sleep(checkCancelledDelayMs)
118-
}
119-
} catch {
120-
case _: CancellationException =>
121-
replReader.interrupt()
122-
process.destroyForcibly()
123-
}
100+
def get(cancelChecker: CancelChecker)(implicit ctx: Context): Option[Evaluator] = {
101+
val classpath = ctx.settings.classpath.value
102+
previousEvaluator match {
103+
case Some(cp, evaluator) if cp == classpath =>
104+
evaluator.reset(cancelChecker)
105+
Some(evaluator)
106+
case _ =>
107+
previousEvaluator.foreach(_._2.exit())
108+
val newEvaluator = javaExec.map(new Evaluator(_, ctx.settings.classpath.value, cancelChecker))
109+
previousEvaluator = newEvaluator.map(jvm => (classpath, jvm))
110+
newEvaluator
124111
}
125112
}
113+
}
114+
115+
/**
116+
* Represents a JVM running the REPL, ready for evaluation.
117+
*
118+
* @param javaExec The path to the `java` executable.
119+
* @param userClasspath The REPL classpath
120+
* @param cancelChecker The token that indicates whether evaluation has been cancelled.
121+
*/
122+
private class Evaluator private (javaExec: String,
123+
userClasspath: String,
124+
cancelChecker: CancelChecker) {
125+
private val process =
126+
new ProcessBuilder(
127+
javaExec,
128+
"-classpath", sys.props("java.class.path"),
129+
dotty.tools.repl.WorksheetMain.getClass.getName.init,
130+
"-classpath", userClasspath,
131+
"-color:never")
132+
.redirectErrorStream(true)
133+
.start()
134+
135+
// The stream that we use to send commands to the REPL
136+
private val processInput = new PrintStream(process.getOutputStream())
137+
138+
// Messages coming out of the REPL
139+
private val processOutput = new ReplReader(process.getInputStream())
140+
processOutput.start()
141+
142+
// The thread that monitors cancellation
143+
private val cancellationThread = new CancellationThread(cancelChecker, this)
144+
cancellationThread.start()
145+
146+
// Wait for the REPL to be ready
147+
processOutput.next()
126148

127149
/**
128-
* Reads the output from the REPL and makes it available via `next()`.
150+
* Submit `command` to the REPL, wait for the result.
129151
*
130-
* @param stream The stream of messages coming out of the REPL.
152+
* @param command The command to evaluate.
153+
* @return The result from the REPL.
131154
*/
132-
private class ReplReader(stream: InputStream) extends Thread {
133-
private val in = new InputStreamReader(stream)
134-
135-
private[this] var output: Option[String] = None
136-
137-
override def run(): Unit = synchronized {
138-
val prompt = "scala> "
139-
val buffer = new StringBuilder
140-
val chars = new Array[Char](256)
141-
var read = 0
142-
143-
while (!Thread.interrupted() && { read = in.read(chars); read >= 0 }) {
144-
buffer.appendAll(chars, 0, read)
145-
if (buffer.endsWith(prompt)) {
146-
output = Some(buffer.toString.stripSuffix(prompt))
147-
buffer.clear()
148-
notify()
149-
wait()
150-
}
155+
def eval(command: String): String = {
156+
processInput.println(command)
157+
processInput.flush()
158+
processOutput.next().trim
159+
}
160+
161+
/**
162+
* Reset the REPL to its initial state, update the cancel checker.
163+
*/
164+
def reset(cancelChecker: CancelChecker): Unit = {
165+
cancellationThread.setCancelChecker(cancelChecker)
166+
eval(":reset")
167+
}
168+
169+
/** Terminate this JVM. */
170+
def exit(): Unit = {
171+
processOutput.interrupt()
172+
process.destroyForcibly()
173+
Evaluator.previousEvaluator = None
174+
cancellationThread.interrupt()
175+
}
176+
177+
}
178+
179+
/**
180+
* Regularly check whether execution has been cancelled, kill REPL if it is.
181+
*/
182+
private class CancellationThread(private[this] var cancelChecker: CancelChecker,
183+
evaluator: Evaluator) extends Thread {
184+
private final val checkCancelledDelayMs = 50
185+
186+
override def run(): Unit = {
187+
try {
188+
while (!Thread.interrupted()) {
189+
cancelChecker.checkCanceled()
190+
Thread.sleep(checkCancelledDelayMs)
151191
}
192+
} catch {
193+
case _: CancellationException => evaluator.exit()
194+
case _: InterruptedException => evaluator.exit()
152195
}
196+
}
197+
198+
def setCancelChecker(cancelChecker: CancelChecker): Unit = {
199+
this.cancelChecker = cancelChecker
200+
}
201+
}
153202

154-
/** Block until the next message is ready. */
155-
def next(): String = synchronized {
156-
while (output.isEmpty) {
203+
/**
204+
* Reads the output from the REPL and makes it available via `next()`.
205+
*
206+
* @param stream The stream of messages coming out of the REPL.
207+
*/
208+
private class ReplReader(stream: InputStream) extends Thread {
209+
private val in = new InputStreamReader(stream)
210+
211+
private[this] var output: Option[String] = None
212+
213+
override def run(): Unit = synchronized {
214+
val prompt = "scala> "
215+
val buffer = new StringBuilder
216+
val chars = new Array[Char](256)
217+
var read = 0
218+
219+
while (!Thread.interrupted() && { read = in.read(chars); read >= 0 }) {
220+
buffer.appendAll(chars, 0, read)
221+
if (buffer.endsWith(prompt)) {
222+
output = Some(buffer.toString.stripSuffix(prompt))
223+
buffer.clear()
224+
notify()
157225
wait()
158226
}
159-
160-
val result = output.get
161-
notify()
162-
output = None
163-
result
164227
}
228+
output = Some("")
229+
notify()
165230
}
166231

232+
/** Block until the next message is ready. */
233+
def next(): String = synchronized {
234+
235+
while (output.isEmpty) {
236+
wait()
237+
}
238+
239+
val result = output.get
240+
notify()
241+
output = None
242+
result
243+
}
167244
}

0 commit comments

Comments
 (0)