Skip to content
This repository was archived by the owner on Sep 8, 2022. It is now read-only.

Commit 7c19dd7

Browse files
committed
Add a mode to reuse JVM for test execution
The cost of VM startup and JIT is the predominant reason that `partest --run` is slow. We're much better of using one thread on the main JVM to execute these tests. This commit uses such a mode under a non-default option. Unforked test execution requires some care: - we need to check the main JVM is started with the mandated immutable System properties (e.g. file.encoding) - other test-specific system properties (e.g. partest.output) need to be patched into the system property map before each test is executed. - sys.exit calls need to be intercepted with a security manager - stdout / stderr need to be intercepted, both in the Java and Scala console printing APIs. Some tests need to be adapted, in particular those that assume that java.class.path will contain the test classes. Tests can also force usage of the legacy forked mode by adding a .javaopts file with a dummy JVM option. Also improve recording of test durations and flag slow tests in console output.
1 parent d8c59ed commit 7c19dd7

File tree

7 files changed

+256
-41
lines changed

7 files changed

+256
-41
lines changed

src/main/scala/scala/tools/partest/PartestDefaults.scala

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,18 @@ object PartestDefaults {
1515

1616
def testBuild = prop("partest.build")
1717
def errorCount = prop("partest.errors") map (_.toInt) getOrElse 0
18-
def numThreads = prop("partest.threads") map (_.toInt) getOrElse runtime.availableProcessors
18+
def numThreads = math.max(1, prop("partest.threads") map (_.toInt) getOrElse runtime.availableProcessors)
19+
def execInProcess: Boolean = {
20+
val prop = java.lang.Boolean.getBoolean("partest.exec.in.process")
21+
if (prop && numThreads > 1) warningMessage
22+
prop
23+
}
24+
private lazy val warningMessage: Unit = {
25+
println("Note: test execution will be non-parallel under -Dpartest.exec.in.process")
26+
}
27+
1928
def waitTime = Duration(prop("partest.timeout") getOrElse "4 hours")
29+
def printDurationThreshold = java.lang.Integer.getInteger("partest.print.duration.threshold.ms", 5000)
2030

2131
//def timeout = "1200000" // per-test timeout
2232

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package scala.tools.partest.nest
2+
3+
import java.io.FileDescriptor
4+
import java.net.InetAddress
5+
import java.security.Permission
6+
7+
class DelegatingSecurityManager(delegate: SecurityManager) extends SecurityManager {
8+
override def checkExit(status: Int): Unit = if (delegate ne null) delegate.checkExit(status)
9+
override def checkPermission(perm: Permission): Unit = if (delegate ne null) delegate.checkPermission(perm)
10+
override def checkPermission(perm: Permission, context: AnyRef): Unit = if (delegate ne null) delegate.checkPermission(perm, context)
11+
override def checkExec(cmd: String): Unit = if (delegate ne null) delegate.checkExec(cmd)
12+
override def checkWrite(file: String): Unit = if (delegate ne null) delegate.checkWrite(file)
13+
override def checkDelete(file: String): Unit = if (delegate ne null) delegate.checkDelete(file)
14+
override def checkRead(file: String): Unit = if (delegate ne null) delegate.checkRead(file)
15+
override def checkRead(file: String, context: scala.Any): Unit = if (delegate ne null) delegate.checkRead(file, context)
16+
override def checkPropertyAccess(key: String): Unit = if (delegate ne null) delegate.checkPropertyAccess(key)
17+
override def checkAccept(host: String, port: Int): Unit = if (delegate ne null) delegate.checkAccept(host, port)
18+
override def checkWrite(fd: FileDescriptor): Unit = if (delegate ne null) delegate.checkWrite(fd)
19+
override def checkPrintJobAccess(): Unit = if (delegate ne null) delegate.checkPrintJobAccess()
20+
override def checkMulticast(maddr: InetAddress): Unit = if (delegate ne null) delegate.checkMulticast(maddr)
21+
override def checkSetFactory(): Unit = if (delegate ne null) delegate.checkSetFactory()
22+
override def checkLink(lib: String): Unit = if (delegate ne null) delegate.checkLink(lib)
23+
override def checkSecurityAccess(target: String): Unit = if (delegate ne null) delegate.checkSecurityAccess(target)
24+
override def checkListen(port: Int): Unit = if (delegate ne null) delegate.checkListen(port)
25+
override def checkAccess(t: Thread): Unit = if (delegate ne null) delegate.checkAccess(t)
26+
override def checkAccess(g: ThreadGroup): Unit = if (delegate ne null) delegate.checkAccess(g)
27+
override def checkCreateClassLoader(): Unit = if (delegate ne null) delegate.checkCreateClassLoader()
28+
override def checkPackageDefinition(pkg: String): Unit = if (delegate ne null) delegate.checkPackageDefinition(pkg)
29+
override def checkConnect(host: String, port: Int): Unit = if (delegate ne null) delegate.checkConnect(host, port)
30+
override def checkConnect(host: String, port: Int, context: scala.Any): Unit = if (delegate ne null) delegate.checkConnect(host, port, context)
31+
override def checkPackageAccess(pkg: String): Unit = if (delegate ne null) delegate.checkPackageAccess(pkg)
32+
override def checkPropertiesAccess(): Unit = if (delegate ne null) delegate.checkPropertiesAccess()
33+
override def checkRead(fd: FileDescriptor): Unit = if (delegate ne null) delegate.checkRead(fd)
34+
}

src/main/scala/scala/tools/partest/nest/NestUI.scala

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ class NestUI(val verbose: Boolean = false, val debug: Boolean = false, val terse
5252
}
5353
}
5454

55-
def statusLine(state: TestState) = {
55+
def statusLine(state: TestState, durationMs: Long) = {
5656
import state._
5757
import TestState._
5858
val colorizer = state match {
@@ -62,10 +62,11 @@ class NestUI(val verbose: Boolean = false, val debug: Boolean = false, val terse
6262
case _ => red
6363
}
6464
val word = bold(colorizer(state.shortStatus))
65-
f"$word $testNumber - $testIdent%-40s$reasonString"
65+
def durationString = if (durationMs > PartestDefaults.printDurationThreshold) f"[duration ${(1.0 * durationMs) / 1000}%.2fs]" else ""
66+
f"$word $testNumber - $testIdent%-40s$reasonString$durationString"
6667
}
6768

68-
def reportTest(state: TestState, info: TestInfo): Unit = {
69+
def reportTest(state: TestState, info: TestInfo, durationMs: Long): Unit = {
6970
if (terse && state.isOk) {
7071
if (dotCount >= DotWidth) {
7172
outline("\n.")
@@ -75,7 +76,7 @@ class NestUI(val verbose: Boolean = false, val debug: Boolean = false, val terse
7576
dotCount += 1
7677
}
7778
} else {
78-
echo(statusLine(state))
79+
echo(statusLine(state, durationMs))
7980
if (!state.isOk) {
8081
def showLog() = if (info.logFile.canRead) {
8182
echo(bold(cyan(s"##### Log file '${info.logFile}' from failed test #####\n")))

src/main/scala/scala/tools/partest/nest/Runner.scala

Lines changed: 122 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -5,24 +5,29 @@
55
package scala.tools.partest
66
package nest
77

8-
import java.io.{ Console => _, _ }
8+
import java.io.{Console => _, _}
9+
import java.lang.reflect.InvocationTargetException
10+
import java.nio.charset.Charset
11+
import java.nio.file.{Files, StandardOpenOption}
912
import java.util.concurrent.Executors
1013
import java.util.concurrent.TimeUnit
1114
import java.util.concurrent.TimeUnit.NANOSECONDS
15+
1216
import scala.collection.mutable.ListBuffer
1317
import scala.concurrent.duration.Duration
1418
import scala.reflect.internal.FatalError
1519
import scala.reflect.internal.util.ScalaClassLoader
16-
import scala.sys.process.{ Process, ProcessLogger }
17-
import scala.tools.nsc.Properties.{ envOrNone, isWin, javaHome, propOrEmpty, versionMsg, javaVmName, javaVmVersion, javaVmInfo }
18-
import scala.tools.nsc.{ Settings, CompilerCommand, Global }
20+
import scala.sys.process.{Process, ProcessLogger}
21+
import scala.tools.nsc.Properties.{envOrNone, isWin, javaHome, javaVmInfo, javaVmName, javaVmVersion, propOrEmpty, versionMsg}
22+
import scala.tools.nsc.{CompilerCommand, Global, Settings}
1923
import scala.tools.nsc.reporters.ConsoleReporter
2024
import scala.tools.nsc.util.stackTraceString
21-
import scala.util.{ Try, Success, Failure }
25+
import scala.util.{Failure, Success, Try}
2226
import ClassPath.join
23-
import TestState.{ Pass, Fail, Crash, Uninitialized, Updated }
24-
25-
import FileManager.{ compareContents, joinPaths, withTempFile }
27+
import TestState.{Crash, Fail, Pass, Uninitialized, Updated}
28+
import FileManager.{compareContents, joinPaths, withTempFile}
29+
import scala.reflect.internal.util.ScalaClassLoader.URLClassLoader
30+
import scala.util.control.ControlThrowable
2631

2732
trait TestInfo {
2833
/** pos/t1234 */
@@ -53,6 +58,7 @@ trait TestInfo {
5358

5459
/** Run a single test. Rubber meets road. */
5560
class Runner(val testFile: File, val suiteRunner: SuiteRunner, val nestUI: NestUI) extends TestInfo {
61+
private val stopwatch = new Stopwatch()
5662

5763
import suiteRunner.{fileManager => fm, _}
5864
val fileManager = fm
@@ -157,8 +163,6 @@ class Runner(val testFile: File, val suiteRunner: SuiteRunner, val nestUI: NestU
157163
if (javaopts.nonEmpty)
158164
nestUI.verbose(s"Found javaopts file '$argsFile', using options: '${javaopts.mkString(",")}'")
159165

160-
val testFullPath = testFile.getAbsolutePath
161-
162166
// Note! As this currently functions, suiteRunner.javaOpts must precede argString
163167
// because when an option is repeated to java only the last one wins.
164168
// That means until now all the .javaopts files were being ignored because
@@ -167,30 +171,15 @@ class Runner(val testFile: File, val suiteRunner: SuiteRunner, val nestUI: NestU
167171
//
168172
// debug: Found javaopts file 'files/shootout/message.scala-2.javaopts', using options: '-Xss32k'
169173
// debug: java -Xss32k -Xss2m -Xms256M -Xmx1024M -classpath [...]
170-
val extras = if (nestUI.debug) List("-Dpartest.debug=true") else Nil
171-
val propertyOptions = List(
172-
"-Dfile.encoding=UTF-8",
173-
"-Djava.library.path="+logFile.getParentFile.getAbsolutePath,
174-
"-Dpartest.output="+outDir.getAbsolutePath,
175-
"-Dpartest.lib="+libraryUnderTest.getAbsolutePath,
176-
"-Dpartest.reflect="+reflectUnderTest.getAbsolutePath,
177-
"-Dpartest.comp="+compilerUnderTest.getAbsolutePath,
178-
"-Dpartest.cwd="+outDir.getParent,
179-
"-Dpartest.test-path="+testFullPath,
180-
"-Dpartest.testname="+fileBase,
181-
"-Djavacmd="+javaCmdPath,
182-
"-Djavaccmd="+javacCmdPath,
183-
"-Duser.language=en",
184-
"-Duser.country=US"
185-
) ++ extras
174+
val propertyOpts = propertyOptions(fork = true).map { case (k, v) => s"-D$k=$v" }
186175

187176
val classpath = joinPaths(extraClasspath ++ testClassPath)
188177

189178
javaCmdPath +: (
190179
(suiteRunner.javaOpts.split(' ') ++ extraJavaOptions ++ javaopts).filter(_ != "").toList ++ Seq(
191180
"-classpath",
192181
join(outDir.toString, classpath)
193-
) ++ propertyOptions ++ Seq(
182+
) ++ propertyOpts ++ Seq(
194183
"scala.tools.nsc.MainGenericRunner",
195184
"-usejavacp",
196185
"Test",
@@ -199,6 +188,40 @@ class Runner(val testFile: File, val suiteRunner: SuiteRunner, val nestUI: NestU
199188
)
200189
}
201190

191+
def propertyOptions(fork: Boolean): List[(String, String)] = {
192+
val testFullPath = testFile.getAbsolutePath
193+
val extras = if (nestUI.debug) List("partest.debug" -> "true") else Nil
194+
val immutablePropsToCheck = List[(String, String)](
195+
"file.encoding" -> "UTF-8",
196+
"user.language" -> "en",
197+
"user.country" -> "US"
198+
)
199+
val immutablePropsForkOnly = List[(String, String)](
200+
"java.library.path" -> logFile.getParentFile.getAbsolutePath,
201+
)
202+
val shared = List(
203+
"partest.output" -> ("" + outDir.getAbsolutePath),
204+
"partest.lib" -> ("" + libraryUnderTest.jfile.getAbsolutePath),
205+
"partest.reflect" -> ("" + reflectUnderTest.jfile.getAbsolutePath),
206+
"partest.comp" -> ("" + compilerUnderTest.jfile.getAbsolutePath),
207+
"partest.cwd" -> ("" + outDir.getParent),
208+
"partest.test-path" -> ("" + testFullPath),
209+
"partest.testname" -> ("" + fileBase),
210+
"javacmd" -> ("" + javaCmdPath),
211+
"javaccmd" -> ("" + javacCmdPath),
212+
) ++ extras
213+
if (fork) {
214+
immutablePropsToCheck ++ immutablePropsForkOnly ++ shared
215+
} else {
216+
for ((k, requiredValue) <- immutablePropsToCheck) {
217+
val actual = System.getProperty(k)
218+
assert(actual == requiredValue, s"Unable to run test without forking as the current JVM has an incorrect system property. For $k, found $actual, required $requiredValue")
219+
}
220+
shared
221+
}
222+
}
223+
224+
202225
/** Runs command redirecting standard out and
203226
* error out to output file.
204227
*/
@@ -235,6 +258,53 @@ class Runner(val testFile: File, val suiteRunner: SuiteRunner, val nestUI: NestU
235258
}
236259
}
237260

261+
def execTestInProcess(classesDir: File, log: File): Boolean = {
262+
stopwatch.pause()
263+
suiteRunner.synchronized {
264+
stopwatch.start()
265+
def run(): Unit = {
266+
StreamCapture.withExtraProperties(propertyOptions(fork = false).toMap) {
267+
try {
268+
val out = Files.newOutputStream(log.toPath, StandardOpenOption.APPEND)
269+
try {
270+
val loader = new URLClassLoader(classesDir.toURI.toURL :: Nil, getClass.getClassLoader)
271+
StreamCapture.capturingOutErr(out) {
272+
val cls = loader.loadClass("Test")
273+
val main = cls.getDeclaredMethod("main", classOf[Array[String]])
274+
try {
275+
main.invoke(null, Array[String]("jvm"))
276+
} catch {
277+
case ite: InvocationTargetException => throw ite.getCause
278+
}
279+
}
280+
} finally {
281+
out.close()
282+
}
283+
} catch {
284+
case t: ControlThrowable => throw t
285+
case t: Throwable =>
286+
// We'll let the checkfile diffing report this failure
287+
Files.write(log.toPath, stackTraceString(t).getBytes(Charset.defaultCharset()), StandardOpenOption.APPEND)
288+
}
289+
}
290+
}
291+
292+
pushTranscript(s"<in process execution of $testIdent> > ${logFile.getName}")
293+
294+
TrapExit(() => run()) match {
295+
case Left((status, throwable)) if status != 0 =>
296+
// Files.readAllLines(log.toPath).forEach(println(_))
297+
// val error = new AssertionError(s"System.exit(${status}) was called.")
298+
// error.setStackTrace(throwable.getStackTrace)
299+
setLastState(genFail("non-zero exit code"))
300+
false
301+
case _ =>
302+
setLastState(genPass())
303+
true
304+
}
305+
}
306+
}
307+
238308
override def toString = s"""Test($testIdent, lastState = $lastState)"""
239309

240310
// result is unused
@@ -641,9 +711,10 @@ class Runner(val testFile: File, val suiteRunner: SuiteRunner, val nestUI: NestU
641711
(diffIsOk, LogContext(logFile, swr, wr))
642712
}
643713

644-
def run(): TestState = {
714+
def run(): (TestState, Long) = {
645715
// javac runner, for one, would merely append to an existing log file, so just delete it before we start
646716
logFile.delete()
717+
stopwatch.start()
647718

648719
if (kind == "neg" || (kind endsWith "-neg")) runNegTest()
649720
else kind match {
@@ -652,10 +723,18 @@ class Runner(val testFile: File, val suiteRunner: SuiteRunner, val nestUI: NestU
652723
case "res" => runResidentTest()
653724
case "scalap" => runScalapTest()
654725
case "script" => runScriptTest()
655-
case _ => runTestCommon(execTest(outDir, logFile) && diffIsOk)
726+
case _ => runRunTest()
656727
}
657728

658-
lastState
729+
(lastState, stopwatch.stop)
730+
}
731+
732+
private def runRunTest(): Unit = {
733+
val argsFile = testFile changeExtension "javaopts"
734+
val javaopts = readOptionsFile(argsFile)
735+
val execInProcess = PartestDefaults.execInProcess && javaopts.isEmpty && !Set("specialized", "instrumented").contains(testFile.getParentFile.getName)
736+
def exec() = if (execInProcess) execTestInProcess(outDir, logFile) else execTest(outDir, logFile)
737+
runTestCommon(exec() && diffIsOk)
659738
}
660739

661740
private def decompileClass(clazz: Class[_], isPackageObject: Boolean): String = {
@@ -738,6 +817,8 @@ class SuiteRunner(
738817
// TODO: make this immutable
739818
PathSettings.testSourcePath = testSourcePath
740819

820+
val durations = collection.concurrent.TrieMap[File, Long]()
821+
741822
def banner = {
742823
val baseDir = fileManager.compilerUnderTest.parent.toString
743824
def relativize(path: String) = path.replace(baseDir, s"$$baseDir").replace(PathSettings.srcDir.toString, "$sourceDir")
@@ -759,29 +840,35 @@ class SuiteRunner(
759840
// |Java Classpath: ${sys.props("java.class.path")}
760841
}
761842

762-
def onFinishTest(testFile: File, result: TestState, durationMs: Long): TestState = result
843+
def onFinishTest(testFile: File, result: TestState, durationMs: Long): TestState = {
844+
durations(testFile) = durationMs
845+
result
846+
}
763847

764848
def runTest(testFile: File): TestState = {
765849
val start = System.nanoTime()
766850
val runner = new Runner(testFile, this, nestUI)
851+
var stopwatchDuration: Option[Long] = None
767852

768853
// when option "--failed" is provided execute test only if log
769854
// is present (which means it failed before)
770855
val state =
771856
if (failed && !runner.logFile.canRead)
772857
runner.genPass()
773858
else {
774-
val (state, _) =
775-
try timed(runner.run())
859+
val (state, durationMs) =
860+
try runner.run()
776861
catch {
777862
case t: Throwable => throw new RuntimeException(s"Error running $testFile", t)
778863
}
779-
nestUI.reportTest(state, runner)
864+
stopwatchDuration = Some(durationMs)
865+
nestUI.reportTest(state, runner, durationMs)
780866
runner.cleanup()
781867
state
782868
}
783869
val end = System.nanoTime()
784-
onFinishTest(testFile, state, TimeUnit.NANOSECONDS.toMillis(end - start))
870+
val durationMs = stopwatchDuration.getOrElse(TimeUnit.NANOSECONDS.toMillis(end - start))
871+
onFinishTest(testFile, state, durationMs)
785872
}
786873

787874
def runTestsForFiles(kindFiles: Array[File], kind: String): Array[TestState] = {
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package scala.tools.partest.nest
2+
3+
class Stopwatch {
4+
private var base: Option[Long] = None
5+
private var elapsed = 0L
6+
def pause(): Unit = {
7+
assert(base.isDefined)
8+
val now = System.nanoTime
9+
elapsed += (now - base.get)
10+
base = None
11+
}
12+
def start(): Unit = {
13+
base = Some(System.nanoTime())
14+
}
15+
16+
def stop(): Long = {
17+
pause()
18+
(1.0 * elapsed / 1000 / 1000).toLong
19+
}
20+
}

0 commit comments

Comments
 (0)