From 375679677c4d07261f99d8cf6a34cd35d2d55b65 Mon Sep 17 00:00:00 2001 From: Ilya Chernikov Date: Mon, 9 Nov 2015 19:51:07 +0100 Subject: [PATCH] Implementation of the new daemon management without inter-daemon elections and with few other simplifications --- .../rmi/kotlinr/KotlinCompilerClient.kt | 40 ++- .../jetbrains/kotlin/rmi/CompileService.kt | 54 +++- .../org/jetbrains/kotlin/rmi/DaemonParams.kt | 17 +- .../kotlin/rmi/service/CompileDaemon.kt | 68 ++---- .../kotlin/rmi/service/CompileServiceImpl.kt | 209 +++++++++++++--- .../kotlin/daemon/CompilerDaemonTest.kt | 230 +++++++++--------- .../compilerRunner/KotlinCompilerRunner.kt | 14 +- 7 files changed, 413 insertions(+), 219 deletions(-) diff --git a/compiler/rmi/kotlinr/src/org/jetbrains/kotlin/rmi/kotlinr/KotlinCompilerClient.kt b/compiler/rmi/kotlinr/src/org/jetbrains/kotlin/rmi/kotlinr/KotlinCompilerClient.kt index 1000e2beff0..4c271ad32cf 100644 --- a/compiler/rmi/kotlinr/src/org/jetbrains/kotlin/rmi/kotlinr/KotlinCompilerClient.kt +++ b/compiler/rmi/kotlinr/src/org/jetbrains/kotlin/rmi/kotlinr/KotlinCompilerClient.kt @@ -42,8 +42,6 @@ public object KotlinCompilerClient { val verboseReporting = System.getProperty(COMPILE_DAEMON_VERBOSE_REPORT_PROPERTY) != null - // TODO: remove jvmStatic after all use sites will switch to kotlin - @JvmStatic public fun connectToCompileService(compilerId: CompilerId, daemonJVMOptions: DaemonJVMOptions, daemonOptions: DaemonOptions, @@ -51,6 +49,29 @@ public object KotlinCompilerClient { autostart: Boolean = true, checkId: Boolean = true ): CompileService? { + fun newFlagFile(): File { + val flagFile = File.createTempFile("kotlin-compiler-client-", "-is-running") + flagFile.deleteOnExit() + return flagFile + } + + val flagFile = System.getProperty(COMPILE_DAEMON_CLIENT_ALIVE_PATH_PROPERTY) + ?.let { it.trimQuotes() } + ?.ifOrNull { !isBlank() } + ?.let { File(it) } + ?.ifOrNull { exists() } + ?: newFlagFile() + return connectToCompileService(compilerId, flagFile, daemonJVMOptions, daemonOptions, reportingTargets, autostart, checkId) + } + + public fun connectToCompileService(compilerId: CompilerId, + clientAliveFlagFile: File, + daemonJVMOptions: DaemonJVMOptions, + daemonOptions: DaemonOptions, + reportingTargets: DaemonReportingTargets, + autostart: Boolean = true, + checkId: Boolean = true + ): CompileService? { var attempts = 0 var fileLock: FileBasedLock? = null @@ -60,6 +81,7 @@ public object KotlinCompilerClient { val service = tryFindDaemon(File(daemonOptions.runFilesPath), compilerId, reportingTargets) if (service != null) { if (!checkId || service.checkCompilerId(compilerId)) { + service.registerClient(clientAliveFlagFile.absolutePath) reportingTargets.report(DaemonReportCategory.DEBUG, "connected to the daemon") return service } @@ -112,6 +134,7 @@ public object KotlinCompilerClient { public fun compile(compilerService: CompileService, + sessionId: Int, targetPlatform: CompileService.TargetPlatform, args: Array, out: OutputStream, @@ -119,11 +142,12 @@ public object KotlinCompilerClient { operationsTracer: RemoteOperationsTracer? = null ): Int { val outStrm = RemoteOutputStreamServer(out, port = port) - return compilerService.remoteCompile(targetPlatform, args, CompilerCallbackServicesFacadeServer(port = port), outStrm, CompileService.OutputFormat.PLAIN, outStrm, operationsTracer) + return compilerService.remoteCompile(sessionId, targetPlatform, args, CompilerCallbackServicesFacadeServer(port = port), outStrm, CompileService.OutputFormat.PLAIN, outStrm, operationsTracer).get() } public fun incrementalCompile(compileService: CompileService, + sessionId: Int, targetPlatform: CompileService.TargetPlatform, args: Array, callbackServices: CompilationServices, @@ -134,6 +158,7 @@ public object KotlinCompilerClient { operationsTracer: RemoteOperationsTracer? = null ): Int = profiler.withMeasure(this) { compileService.remoteIncrementalCompile( + sessionId, targetPlatform, args, CompilerCallbackServicesFacadeServer(incrementalCompilationComponents = callbackServices.incrementalCompilationComponents, @@ -142,7 +167,7 @@ public object KotlinCompilerClient { RemoteOutputStreamServer(compilerOut, port), CompileService.OutputFormat.XML, RemoteOutputStreamServer(daemonOut, port), - operationsTracer) + operationsTracer).get() } public val COMPILE_DAEMON_CLIENT_OPTIONS_PROPERTY: String = "kotlin.daemon.client.options" @@ -211,14 +236,14 @@ public object KotlinCompilerClient { val outStrm = RemoteOutputStreamServer(System.out) val servicesFacade = CompilerCallbackServicesFacadeServer() try { - val memBefore = daemon.getUsedMemory() / 1024 + val memBefore = daemon.getUsedMemory().get() / 1024 val startTime = System.nanoTime() - val res = daemon.remoteCompile(CompileService.TargetPlatform.JVM, filteredArgs.toArrayList().toTypedArray(), servicesFacade, outStrm, CompileService.OutputFormat.PLAIN, outStrm, null) + val res = daemon.remoteCompile(CompileService.NO_SESSION, CompileService.TargetPlatform.JVM, filteredArgs.toArrayList().toTypedArray(), servicesFacade, outStrm, CompileService.OutputFormat.PLAIN, outStrm, null) val endTime = System.nanoTime() println("Compilation result code: $res") - val memAfter = daemon.getUsedMemory() / 1024 + val memAfter = daemon.getUsedMemory().get() / 1024 println("Compilation time: " + TimeUnit.NANOSECONDS.toMillis(endTime - startTime) + " ms") println("Used memory $memAfter (${"%+d".format(memAfter - memBefore)} kb)") } @@ -430,3 +455,4 @@ internal fun isProcessAlive(process: Process) = true } +internal fun T.ifOrNull(pred: T.() -> Boolean): T? = if (this.pred()) this else null diff --git a/compiler/rmi/rmi-interface/src/org/jetbrains/kotlin/rmi/CompileService.kt b/compiler/rmi/rmi-interface/src/org/jetbrains/kotlin/rmi/CompileService.kt index b2ba6d764b0..cb1d8b3c1d2 100644 --- a/compiler/rmi/rmi-interface/src/org/jetbrains/kotlin/rmi/CompileService.kt +++ b/compiler/rmi/rmi-interface/src/org/jetbrains/kotlin/rmi/CompileService.kt @@ -16,32 +16,71 @@ package org.jetbrains.kotlin.rmi +import java.io.Serializable import java.rmi.Remote import java.rmi.RemoteException public interface CompileService : Remote { - public enum class OutputFormat : java.io.Serializable { + public enum class OutputFormat : Serializable { PLAIN, XML } - public enum class TargetPlatform : java.io.Serializable { + public enum class TargetPlatform : Serializable { JVM, JS } + companion object { + public val NO_SESSION: Int = 0 + } + + public sealed class CallResult : Serializable { + class Good(val result: R) : CallResult() + class Ok : CallResult() + class Dying : CallResult() + class Error(val message: String) : CallResult() + fun get(): R = when (this) { + is Good -> this.result + is Dying -> throw IllegalStateException("Service is dying") + is Error -> throw IllegalStateException(this.message) + else -> throw IllegalStateException("Unknown state") + } + } + + // TODO: remove! @Throws(RemoteException::class) - public fun checkCompilerId(compilerId: CompilerId): Boolean + public fun checkCompilerId(expectedCompilerId: CompilerId): Boolean @Throws(RemoteException::class) - public fun getUsedMemory(): Long + public fun getUsedMemory(): CallResult @Throws(RemoteException::class) - public fun shutdown() + public fun getDaemonOptions(): CallResult + + @Throws(RemoteException::class) + public fun getDaemonJVMOptions(): CallResult + + @Throws(RemoteException::class) + public fun registerClient(aliveFlagPath: String?): CallResult + + // TODO: consider adding another client alive checking mechanism, e.g. socket/port + + @Throws(RemoteException::class) + public fun leaseCompileSession(aliveFlagPath: String?): CallResult + + @Throws(RemoteException::class) + public fun releaseCompileSession(sessionId: Int): Unit + + @Throws(RemoteException::class) + public fun shutdown(): Unit + + // TODO: consider adding async version of shutdown and release @Throws(RemoteException::class) public fun remoteCompile( + sessionId: Int, targetPlatform: TargetPlatform, args: Array, servicesFacade: CompilerCallbackServicesFacade, @@ -49,10 +88,11 @@ public interface CompileService : Remote { outputFormat: OutputFormat, serviceOutputStream: RemoteOutputStream, operationsTracer: RemoteOperationsTracer? - ): Int + ): CallResult @Throws(RemoteException::class) public fun remoteIncrementalCompile( + sessionId: Int, targetPlatform: TargetPlatform, args: Array, servicesFacade: CompilerCallbackServicesFacade, @@ -60,5 +100,5 @@ public interface CompileService : Remote { compilerOutputFormat: OutputFormat, serviceOutputStream: RemoteOutputStream, operationsTracer: RemoteOperationsTracer? - ): Int + ): CallResult } diff --git a/compiler/rmi/rmi-interface/src/org/jetbrains/kotlin/rmi/DaemonParams.kt b/compiler/rmi/rmi-interface/src/org/jetbrains/kotlin/rmi/DaemonParams.kt index 8f68d522729..23b526ba5b9 100644 --- a/compiler/rmi/rmi-interface/src/org/jetbrains/kotlin/rmi/DaemonParams.kt +++ b/compiler/rmi/rmi-interface/src/org/jetbrains/kotlin/rmi/DaemonParams.kt @@ -44,9 +44,11 @@ public val COMPILE_DAEMON_STARTUP_TIMEOUT_PROPERTY: String = "kotlin.daemon.star public val COMPILE_DAEMON_DEFAULT_FILES_PREFIX: String = "kotlin-daemon" public val COMPILE_DAEMON_TIMEOUT_INFINITE_S: Int = 0 public val COMPILE_DAEMON_DEFAULT_IDLE_TIMEOUT_S: Int = 7200 // 2 hours +public val COMPILE_DAEMON_DEFAULT_UNUSED_TIMEOUT_S: Int = 60 +public val COMPILE_DAEMON_DEFAULT_SHUTDOWN_DELAY_MS: Long = 1000L // 1 sec public val COMPILE_DAEMON_MEMORY_THRESHOLD_INFINITE: Long = 0L public val COMPILE_DAEMON_FORCE_SHUTDOWN_DEFAULT_TIMEOUT_MS: Long = 10000L // 10 secs -public val COMPILE_DAEMON_FORCE_SHUTDOWN_TIMEOUT_INFINITE: Long = 0L +public val COMPILE_DAEMON_TIMEOUT_INFINITE_MS: Long = 0L public val COMPILE_DAEMON_DEFAULT_RUN_DIR_PATH: String get() = FileSystem.getRuntimeStateFilesPath("kotlin", "daemon") @@ -204,8 +206,9 @@ public data class DaemonOptions( public var runFilesPath: String = COMPILE_DAEMON_DEFAULT_RUN_DIR_PATH, public var autoshutdownMemoryThreshold: Long = COMPILE_DAEMON_MEMORY_THRESHOLD_INFINITE, public var autoshutdownIdleSeconds: Int = COMPILE_DAEMON_DEFAULT_IDLE_TIMEOUT_S, + public var autoshutdownUnusedSeconds: Int = COMPILE_DAEMON_DEFAULT_UNUSED_TIMEOUT_S, + public var shutdownDelayMilliseconds: Long = COMPILE_DAEMON_DEFAULT_SHUTDOWN_DELAY_MS, public var forceShutdownTimeoutMilliseconds: Long = COMPILE_DAEMON_FORCE_SHUTDOWN_DEFAULT_TIMEOUT_MS, - public var clientAliveFlagPath: String? = null, public var verbose: Boolean = false, public var reportPerf: Boolean = false ) : OptionsGroup { @@ -213,9 +216,11 @@ public data class DaemonOptions( override val mappers: List> get() = listOf(PropMapper(this, DaemonOptions::runFilesPath, fromString = { it.trimQuotes() }), PropMapper(this, DaemonOptions::autoshutdownMemoryThreshold, fromString = { it.toLong() }, skipIf = { it == 0L }, mergeDelimiter = "="), + // TODO: implement "use default" value without specifying default, so if client and server uses different defaults, it should not lead to many params in the cmd line; use 0 for it and used different val for infinite PropMapper(this, DaemonOptions::autoshutdownIdleSeconds, fromString = { it.toInt() }, skipIf = { it == 0 }, mergeDelimiter = "="), + PropMapper(this, DaemonOptions::autoshutdownUnusedSeconds, fromString = { it.toInt() }, skipIf = { it == COMPILE_DAEMON_DEFAULT_UNUSED_TIMEOUT_S }, mergeDelimiter = "="), + PropMapper(this, DaemonOptions::shutdownDelayMilliseconds, fromString = { it.toLong() }, skipIf = { it == COMPILE_DAEMON_DEFAULT_SHUTDOWN_DELAY_MS }, mergeDelimiter = "="), PropMapper(this, DaemonOptions::forceShutdownTimeoutMilliseconds, fromString = { it.toLong() }, skipIf = { it == COMPILE_DAEMON_FORCE_SHUTDOWN_DEFAULT_TIMEOUT_MS }, mergeDelimiter = "="), - NullablePropMapper(this, DaemonOptions::clientAliveFlagPath, fromString = { it }, toString = { "${it?.trimQuotes()}" }, mergeDelimiter = "="), BoolPropMapper(this, DaemonOptions::verbose), BoolPropMapper(this, DaemonOptions::reportPerf)) } @@ -295,12 +300,6 @@ public fun configureDaemonOptions(opts: DaemonOptions): DaemonOptions { "Unrecognized daemon options passed via property $COMPILE_DAEMON_OPTIONS_PROPERTY: " + unrecognized.joinToString(" ") + "\nSupported options: " + opts.mappers.joinToString(", ", transform = { it.names.first() })) } - System.getProperty(COMPILE_DAEMON_CLIENT_ALIVE_PATH_PROPERTY)?.let { - val trimmed = it.trimQuotes() - if (!trimmed.isBlank()) { - opts.clientAliveFlagPath = trimmed - } - } System.getProperty(COMPILE_DAEMON_VERBOSE_REPORT_PROPERTY)?.let { opts.verbose = true } System.getProperty(COMPILE_DAEMON_REPORT_PERF_PROPERTY)?.let { opts.reportPerf = true } return opts diff --git a/compiler/rmi/rmi-server/src/org/jetbrains/kotlin/rmi/service/CompileDaemon.kt b/compiler/rmi/rmi-server/src/org/jetbrains/kotlin/rmi/service/CompileDaemon.kt index 5fccafeff5e..3108995bfdd 100644 --- a/compiler/rmi/rmi-server/src/org/jetbrains/kotlin/rmi/service/CompileDaemon.kt +++ b/compiler/rmi/rmi-server/src/org/jetbrains/kotlin/rmi/service/CompileDaemon.kt @@ -33,7 +33,6 @@ import java.util.logging.Level import java.util.logging.LogManager import java.util.logging.Logger import kotlin.concurrent.schedule -import kotlin.concurrent.timer val DAEMON_PERIODIC_CHECK_INTERVAL_MS = 1000L @@ -101,6 +100,8 @@ public object CompileDaemon { val daemonOptions = DaemonOptions() try { + val daemonJVMOptions = configureDaemonJVMOptions(inheritMemoryLimits = true, inheritAdditionalProperties = true) + val filteredArgs = args.asIterable().filterExtractProps(compilerId, daemonOptions, prefix = COMPILE_DAEMON_CMDLINE_OPTIONS_PREFIX) if (filteredArgs.any()) { @@ -120,18 +121,6 @@ public object CompileDaemon { // setDaemonPermissions(daemonOptions.port) val (registry, port) = findPortAndCreateRegistry(COMPILE_DAEMON_FIND_PORT_ATTEMPTS, COMPILE_DAEMON_PORTS_RANGE_START, COMPILE_DAEMON_PORTS_RANGE_END) - val runFileDir = File(if (daemonOptions.runFilesPath.isBlank()) COMPILE_DAEMON_DEFAULT_RUN_DIR_PATH else daemonOptions.runFilesPath) - runFileDir.mkdirs() - val runFile = File(runFileDir, - makeRunFilenameString(timestamp = "%tFT% js } } - val compilerService = CompileServiceImpl(registry, compilerSelector, compilerId, daemonOptions, port, + // timer with a daemon thread, meaning it should not prevent JVM to exit normally + val timer = Timer(true) + val compilerService = CompileServiceImpl(registry = registry, + compiler = compilerSelector, + compilerId = compilerId, + daemonOptions = daemonOptions, + daemonJVMOptions = daemonJVMOptions, + port = port, + timer = timer, onShutdown = { - if (daemonOptions.forceShutdownTimeoutMilliseconds != COMPILE_DAEMON_FORCE_SHUTDOWN_TIMEOUT_INFINITE) { + if (daemonOptions.forceShutdownTimeoutMilliseconds != COMPILE_DAEMON_TIMEOUT_INFINITE_MS) { // running a watcher thread that ensures that if the daemon is not exited normally (may be due to RMI leftovers), it's forced to exit - // the watcher is a daemon thread, meaning it should not prevent JVM to exit normally - Timer(true).schedule(daemonOptions.forceShutdownTimeoutMilliseconds) { + timer.schedule(daemonOptions.forceShutdownTimeoutMilliseconds) { + cancel() log.info("force JVM shutdown") System.exit(0) } } + else { + timer.cancel() + } }) if (daemonOptions.runFilesPath.isNotEmpty()) println(daemonOptions.runFilesPath) - daemonOptions.clientAliveFlagPath?.let { - if (!File(it).exists()) { - log.info("Client alive flag $it do not exist, disable watching it") - daemonOptions.clientAliveFlagPath = null - } - } - // this supposed to stop redirected streams reader(s) on the client side and prevent some situations with hanging threads, but doesn't work reliably // TODO: implement more reliable scheme System.out.close() @@ -170,33 +163,6 @@ public object CompileDaemon { System.setErr(PrintStream(LogStream("stderr"))) System.setOut(PrintStream(LogStream("stdout"))) - - fun shutdownCondition(check: () -> Boolean, message: String): Boolean { - val res = check() - if (res) { - log.info(message) - } - return res - } - - // stopping daemon if any shutdown condition is met - timer(initialDelay = DAEMON_PERIODIC_CHECK_INTERVAL_MS, period = DAEMON_PERIODIC_CHECK_INTERVAL_MS, daemon = true) { - try { - val idleSeconds = nowSeconds() - compilerService.lastUsedSeconds - if (shutdownCondition({ !runFile.exists() }, "Run file removed, shutting down") || - shutdownCondition({ daemonOptions.autoshutdownIdleSeconds != COMPILE_DAEMON_TIMEOUT_INFINITE_S && idleSeconds > daemonOptions.autoshutdownIdleSeconds }, - "Idle timeout exceeded ${daemonOptions.autoshutdownIdleSeconds}s, shutting down") || - shutdownCondition({ daemonOptions.clientAliveFlagPath?.let { !File(it).exists() } ?: false }, - "Client alive flag ${daemonOptions.clientAliveFlagPath} removed, shutting down")) { - cancel() - compilerService.shutdown() - } - } - catch (e: Exception) { - System.err.println("Exception in timer thread: " + e.getMessage()) - e.printStackTrace(System.err) - } - } } catch (e: Exception) { System.err.println("Exception: " + e.getMessage()) diff --git a/compiler/rmi/rmi-server/src/org/jetbrains/kotlin/rmi/service/CompileServiceImpl.kt b/compiler/rmi/rmi-server/src/org/jetbrains/kotlin/rmi/service/CompileServiceImpl.kt index 16ee62e0dfb..1025e20b6b5 100644 --- a/compiler/rmi/rmi-server/src/org/jetbrains/kotlin/rmi/service/CompileServiceImpl.kt +++ b/compiler/rmi/rmi-server/src/org/jetbrains/kotlin/rmi/service/CompileServiceImpl.kt @@ -23,14 +23,19 @@ import org.jetbrains.kotlin.load.kotlin.incremental.components.IncrementalCompil import org.jetbrains.kotlin.progress.CompilationCanceledStatus import org.jetbrains.kotlin.rmi.* import java.io.BufferedOutputStream +import java.io.File import java.io.PrintStream import java.rmi.NoSuchObjectException import java.rmi.registry.Registry import java.rmi.server.UnicastRemoteObject +import java.util.* import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.locks.ReentrantReadWriteLock +import java.util.logging.Level import java.util.logging.Logger import kotlin.concurrent.read +import kotlin.concurrent.schedule import kotlin.concurrent.write fun nowSeconds() = TimeUnit.NANOSECONDS.toSeconds(System.nanoTime()) @@ -58,24 +63,104 @@ private class EventMangerImpl : EventManger { class CompileServiceImpl( val registry: Registry, val compiler: CompilerSelector, - val selfCompilerId: CompilerId, + val compilerId: CompilerId, val daemonOptions: DaemonOptions, + val daemonJVMOptions: DaemonJVMOptions, port: Int, + val timer: Timer, val onShutdown: () -> Unit ) : CompileService { - private val classpathWatcher = LazyClasspathWatcher(selfCompilerId.compilerClasspath) + // wrapped in a class to encapsulate alive check logic + private class ClientOrSessionProxy(val aliveFlagPath: String?) { + val registered = nowSeconds() + val secondsSinceRegistered: Long get() = nowSeconds() - registered + val isAlive: Boolean get() = aliveFlagPath?.let { File(it).exists() } ?: true // assuming that if no file was given, the client is alive + } + + private val clientProxies: MutableSet = hashSetOf() + private val sessions: MutableMap = hashMapOf() + + private val sessionsIdCounter: AtomicInteger = AtomicInteger(0) + private val internalRng = Random() + + private val classpathWatcher = LazyClasspathWatcher(compilerId.compilerClasspath) + + private val compilationsCounter: AtomicInteger = AtomicInteger(0) + @Volatile private var _lastUsedSeconds = nowSeconds() + public val lastUsedSeconds: Long get() = if (rwlock.isWriteLocked || rwlock.readLockCount - rwlock.readHoldCount > 0) nowSeconds() else _lastUsedSeconds + + val log by lazy { Logger.getLogger("compiler") } + + private val rwlock = ReentrantReadWriteLock() + private var alive = false + + private var runFile: File + + init { + val runFileDir = File(if (daemonOptions.runFilesPath.isBlank()) COMPILE_DAEMON_DEFAULT_RUN_DIR_PATH else daemonOptions.runFilesPath) + runFileDir.mkdirs() + runFile = File(runFileDir, + makeRunFilenameString(timestamp = "%tFT% = ifAlive { daemonOptions } - override fun getUsedMemory(): Long = ifAlive { usedMemory(withGC = true) } + override fun getDaemonJVMOptions(): CompileService.CallResult = ifAlive { daemonJVMOptions } + + override fun registerClient(aliveFlagPath: String?): CompileService.CallResult = ifAlive_Nothing { + synchronized(clientProxies) { + clientProxies.add(ClientOrSessionProxy(aliveFlagPath)) + } + } + + // TODO: consider tying a session to a client and use this info to cleanup + override fun leaseCompileSession(aliveFlagPath: String?): CompileService.CallResult = ifAlive { + // fighting hypothetical integer wrapping + var newId = sessionsIdCounter.incrementAndGet() + val session = ClientOrSessionProxy(aliveFlagPath) + for (attempt in 1..100) { + if (newId != CompileService.NO_SESSION) { + synchronized(sessions) { + if (!sessions.containsKey(newId)) { + sessions.put(newId, session) + return@ifAlive newId + } + } + } + // assuming wrap, jumping to random number to reduce probability of further ckashes + newId = sessionsIdCounter.addAndGet(internalRng.nextInt()) + } + throw Exception("Invalid state or algorithm error") + } + + override fun releaseCompileSession(sessionId: Int) { + synchronized(sessions) { + sessions.remove(sessionId) + // TODO: some cleanup goes here + if (sessions.isEmpty()) { + // TODO: and some goes here + } + } + periodicAndAfterSessionCheck() + } + + override fun checkCompilerId(expectedCompilerId: CompilerId): Boolean = + (compilerId.compilerVersion.isEmpty() || compilerId.compilerVersion == expectedCompilerId.compilerVersion) && + (compilerId.compilerClasspath.all { expectedCompilerId.compilerClasspath.contains(it) }) && + !classpathWatcher.isChanged + + override fun getUsedMemory(): CompileService.CallResult = ifAlive { usedMemory(withGC = true) } override fun shutdown() { ifAliveExclusive { @@ -87,30 +172,32 @@ class CompileServiceImpl( } } - override fun remoteCompile(targetPlatform: CompileService.TargetPlatform, + override fun remoteCompile(sessionId: Int, + targetPlatform: CompileService.TargetPlatform, args: Array, servicesFacade: CompilerCallbackServicesFacade, compilerOutputStream: RemoteOutputStream, outputFormat: CompileService.OutputFormat, serviceOutputStream: RemoteOutputStream, operationsTracer: RemoteOperationsTracer? - ): Int = - doCompile(args, compilerOutputStream, serviceOutputStream, operationsTracer) { printStream, eventManager, profiler -> + ): CompileService.CallResult = + doCompile(sessionId, args, compilerOutputStream, serviceOutputStream, operationsTracer) { printStream, eventManager, profiler -> when (outputFormat) { CompileService.OutputFormat.PLAIN -> compiler[targetPlatform].exec(printStream, *args) CompileService.OutputFormat.XML -> compiler[targetPlatform].execAndOutputXml(printStream, createCompileServices(servicesFacade, eventManager, profiler), *args) } } - override fun remoteIncrementalCompile(targetPlatform: CompileService.TargetPlatform, + override fun remoteIncrementalCompile(sessionId: Int, + targetPlatform: CompileService.TargetPlatform, args: Array, servicesFacade: CompilerCallbackServicesFacade, compilerOutputStream: RemoteOutputStream, compilerOutputFormat: CompileService.OutputFormat, serviceOutputStream: RemoteOutputStream, operationsTracer: RemoteOperationsTracer? - ): Int = - doCompile(args, compilerOutputStream, serviceOutputStream, operationsTracer) { printStream, eventManager, profiler -> + ): CompileService.CallResult = + doCompile(sessionId, args, compilerOutputStream, serviceOutputStream, operationsTracer) { printStream, eventManager, profiler -> when (compilerOutputFormat) { CompileService.OutputFormat.PLAIN -> throw NotImplementedError("Only XML output is supported in remote incremental compilation") CompileService.OutputFormat.XML -> compiler[targetPlatform].execAndOutputXml(printStream, createCompileServices(servicesFacade, eventManager, profiler), *args) @@ -119,14 +206,6 @@ class CompileServiceImpl( // internal implementation stuff - @Volatile private var _lastUsedSeconds = nowSeconds() - public val lastUsedSeconds: Long get() = if (rwlock.isWriteLocked || rwlock.readLockCount - rwlock.readHoldCount > 0) nowSeconds() else _lastUsedSeconds - - val log by lazy { Logger.getLogger("compiler") } - - private val rwlock = ReentrantReadWriteLock() - private var alive = false - // TODO: consider matching compilerId coming from outside with actual one // private val selfCompilerId by lazy { // CompilerId( @@ -153,16 +232,79 @@ class CompileServiceImpl( val stub = UnicastRemoteObject.exportObject(this, port, LoopbackNetworkInterface.clientLoopbackSocketFactory, LoopbackNetworkInterface.serverLoopbackSocketFactory) as CompileService registry.rebind (COMPILER_SERVICE_RMI_NAME, stub); alive = true + + timer.schedule(delay = DAEMON_PERIODIC_CHECK_INTERVAL_MS, period = DAEMON_PERIODIC_CHECK_INTERVAL_MS) { + try { + periodicAndAfterSessionCheck() + } + catch (e: Exception) { + System.err.println("Exception in timer thread: " + e.message) + e.printStackTrace(System.err) + log.log(Level.SEVERE, "Exception in timer thread", e) + } + } } - private fun doCompile(args: Array, + private fun periodicAndAfterSessionCheck() { + + // 1. check if unused for a timeout - shutdown + if (shutdownCondition({ daemonOptions.autoshutdownUnusedSeconds != COMPILE_DAEMON_TIMEOUT_INFINITE_S && compilationsCounter.get() == 0 && nowSeconds() - lastUsedSeconds > daemonOptions.autoshutdownUnusedSeconds }, + "Unused timeout exceeded ${daemonOptions.autoshutdownUnusedSeconds}s, shutting down")) { + shutdown() + } + else { + synchronized(sessions) { + // 2. check if any session hanged - clean + // making copy of the list before calling release + sessions.filterValues { !it.isAlive }.keys.toArrayList() + }.forEach { releaseCompileSession(it) } + + // 3. clean dead clients, then check if any left - conditional shutdown (with small delay) + synchronized(clientProxies) { clientProxies.removeAll( clientProxies.filter{ !it.isAlive }) } + if (clientProxies.none()) { + shutdownWithDelay() + } + // 4. check idle timeout - shutdown + if (shutdownCondition({ daemonOptions.autoshutdownIdleSeconds != COMPILE_DAEMON_TIMEOUT_INFINITE_S && nowSeconds() - lastUsedSeconds > daemonOptions.autoshutdownIdleSeconds }, + "Idle timeout exceeded ${daemonOptions.autoshutdownIdleSeconds}s, shutting down") || + // 5. discovery file removed - shutdown + shutdownCondition({ !runFile.exists() }, "Run file removed, shutting down") || + // 6. compiler changed (seldom check) - shutdown + // TODO: could be too expensive anyway, consider removing this check + shutdownCondition({ classpathWatcher.isChanged }, "Compiler changed")) + { + shutdown() + } + } + } + + private fun shutdownWithDelay() { + val currentCompilationsCount = compilationsCounter.get() + timer.schedule(daemonOptions.shutdownDelayMilliseconds) { + if (currentCompilationsCount == compilationsCounter.get()) { + shutdown() + } + } + } + + private fun shutdownCondition(check: () -> Boolean, message: String): Boolean { + val res = check() + if (res) { + log.info(message) + } + return res + } + + private fun doCompile(sessionId: Int, + args: Array, compilerMessagesStreamProxy: RemoteOutputStream, serviceOutputStreamProxy: RemoteOutputStream, operationsTracer: RemoteOperationsTracer?, - body: (PrintStream, EventManger, Profiler) -> ExitCode): Int = + body: (PrintStream, EventManger, Profiler) -> ExitCode): CompileService.CallResult = ifAlive { operationsTracer?.before("compile") + compilationsCounter.incrementAndGet() val rpcProfiler = if (daemonOptions.reportPerf) WallAndThreadTotalProfiler() else DummyProfiler() val eventManger = EventMangerImpl() val compilerMessagesStream = PrintStream(BufferedOutputStream(RemoteOutputStreamClient(compilerMessagesStreamProxy, rpcProfiler), 4096)) @@ -236,13 +378,20 @@ class CompileServiceImpl( } } - fun ifAlive(body: () -> R): R = rwlock.read { - if (!alive) throw IllegalStateException("Kotlin Compiler Service is not in alive state") - body() + fun ifAlive(body: () -> R): CompileService.CallResult = rwlock.read { + if (!alive) CompileService.CallResult.Dying() + CompileService.CallResult.Good( body() ) } - fun ifAliveExclusive(body: () -> R): R = rwlock.write { - if (!alive) throw IllegalStateException("Kotlin Compiler Service is not in alive state") + // TODO: find how to implement it without using unique name for this variant; making name deliberately ugly meanwhile + fun ifAlive_Nothing(body: () -> Unit): CompileService.CallResult = rwlock.read { + if (!alive) CompileService.CallResult.Dying() body() + CompileService.CallResult.Ok() + } + + fun ifAliveExclusive(body: () -> R): CompileService.CallResult = rwlock.write { + if (!alive) CompileService.CallResult.Dying() + CompileService.CallResult.Good( body() ) } } diff --git a/compiler/tests/org/jetbrains/kotlin/daemon/CompilerDaemonTest.kt b/compiler/tests/org/jetbrains/kotlin/daemon/CompilerDaemonTest.kt index 71d23e6320b..e18630fdf96 100644 --- a/compiler/tests/org/jetbrains/kotlin/daemon/CompilerDaemonTest.kt +++ b/compiler/tests/org/jetbrains/kotlin/daemon/CompilerDaemonTest.kt @@ -43,18 +43,19 @@ public class CompilerDaemonTest : KotlinIntegrationTestBase() { File(KotlinIntegrationTestBase.getCompilerLib(), "kotlin-compiler.jar")) val compilerId by lazy(LazyThreadSafetyMode.NONE) { CompilerId.makeCompilerId(compilerClassPath) } - private fun compileOnDaemon(compilerId: CompilerId, daemonJVMOptions: DaemonJVMOptions, daemonOptions: DaemonOptions, vararg args: String): CompilerResults { - val daemon = KotlinCompilerClient.connectToCompileService(compilerId, daemonJVMOptions, daemonOptions, DaemonReportingTargets(out = System.err), autostart = true, checkId = true) + private fun compileOnDaemon(clientAliveFile: File, compilerId: CompilerId, daemonJVMOptions: DaemonJVMOptions, daemonOptions: DaemonOptions, vararg args: String): CompilerResults { + val daemon = KotlinCompilerClient.connectToCompileService(compilerId, clientAliveFile, daemonJVMOptions, daemonOptions, DaemonReportingTargets(out = System.err), autostart = true, checkId = true) TestCase.assertNotNull("failed to connect daemon", daemon) + daemon?.registerClient(clientAliveFile.absolutePath) val strm = ByteArrayOutputStream() - val code = KotlinCompilerClient.compile(daemon!!, CompileService.TargetPlatform.JVM, args, strm) + val code = KotlinCompilerClient.compile(daemon!!, CompileService.NO_SESSION, CompileService.TargetPlatform.JVM, args, strm) return CompilerResults(code, strm.toString()) } - private fun runDaemonCompilerTwice(compilerId: CompilerId, daemonJVMOptions: DaemonJVMOptions, daemonOptions: DaemonOptions, vararg args: String): Unit { - val res1 = compileOnDaemon(compilerId, daemonJVMOptions, daemonOptions, *args) + private fun runDaemonCompilerTwice(clientAliveFile: File, compilerId: CompilerId, daemonJVMOptions: DaemonJVMOptions, daemonOptions: DaemonOptions, vararg args: String): Unit { + val res1 = compileOnDaemon(clientAliveFile, compilerId, daemonJVMOptions, daemonOptions, *args) TestCase.assertEquals("first compilation failed:\n${res1.out}", 0, res1.resultCode) - val res2 = compileOnDaemon(compilerId, daemonJVMOptions, daemonOptions, *args) + val res2 = compileOnDaemon(clientAliveFile, compilerId, daemonJVMOptions, daemonOptions, *args) TestCase.assertEquals("second compilation failed:\n${res2.out}", 0, res2.resultCode) TestCase.assertEquals("build results differ", CliBaseTest.removePerfOutput(res1.out), CliBaseTest.removePerfOutput(res2.out)) } @@ -67,50 +68,53 @@ public class CompilerDaemonTest : KotlinIntegrationTestBase() { public fun testHelloApp() { val flagFile = createTempFile(getTestName(true), ".alive") - flagFile.deleteOnExit() - val daemonOptions = DaemonOptions(runFilesPath = File(tmpdir, getTestName(true)).absolutePath, - clientAliveFlagPath = flagFile.absolutePath, - verbose = true, - reportPerf = true) - - KotlinCompilerClient.shutdownCompileService(compilerId, daemonOptions) - - val logFile = createTempFile("kotlin-daemon-test.", ".log") - - val daemonJVMOptions = configureDaemonJVMOptions("D$COMPILE_DAEMON_LOG_PATH_PROPERTY=\"${logFile.absolutePath}\"", - inheritMemoryLimits = false, inheritAdditionalProperties = false) - var daemonShotDown = false - try { - val jar = tmpdir.absolutePath + File.separator + "hello.jar" - runDaemonCompilerTwice(compilerId, daemonJVMOptions, daemonOptions, - "-include-runtime", File(getTestBaseDir(), "hello.kt").absolutePath, "-d", jar) + val daemonOptions = DaemonOptions(runFilesPath = File(tmpdir, getTestName(true)).absolutePath, + verbose = true, + reportPerf = true) KotlinCompilerClient.shutdownCompileService(compilerId, daemonOptions) - daemonShotDown = true - var compileTime1 = 0L - var compileTime2 = 0L - logFile.reader().useLines { - it.ifNotContainsSequence( LinePattern("Kotlin compiler daemon version"), - LinePattern("Starting compilation with args: "), - LinePattern("Compile on daemon: (\\d+) ms", { it.groups.get(1)?.value?.toLong()?.let { compileTime1 = it }; true } ), - LinePattern("Starting compilation with args: "), - LinePattern("Compile on daemon: (\\d+) ms", { it.groups.get(1)?.value?.toLong()?.let { compileTime2 = it }; true } ), - LinePattern("Shutdown complete")) - { unmatchedPattern, lineNo -> - TestCase.fail("pattern not found in the input: " + unmatchedPattern.regex + - "\nunmatched part of the log file (" + logFile.absolutePath + - ") from line " + lineNo + ":\n\n" + logFile.reader().useLines { it.drop(lineNo).joinToString("\n") }) + + val logFile = createTempFile("kotlin-daemon-test.", ".log") + + val daemonJVMOptions = configureDaemonJVMOptions("D$COMPILE_DAEMON_LOG_PATH_PROPERTY=\"${logFile.absolutePath}\"", + inheritMemoryLimits = false, inheritAdditionalProperties = false) + var daemonShotDown = false + + try { + val jar = tmpdir.absolutePath + File.separator + "hello.jar" + runDaemonCompilerTwice(flagFile, compilerId, daemonJVMOptions, daemonOptions, + "-include-runtime", File(getTestBaseDir(), "hello.kt").absolutePath, "-d", jar) + + KotlinCompilerClient.shutdownCompileService(compilerId, daemonOptions) + daemonShotDown = true + var compileTime1 = 0L + var compileTime2 = 0L + logFile.reader().useLines { + it.ifNotContainsSequence(LinePattern("Kotlin compiler daemon version"), + LinePattern("Starting compilation with args: "), + LinePattern("Compile on daemon: (\\d+) ms", { it.groups.get(1)?.value?.toLong()?.let { compileTime1 = it }; true }), + LinePattern("Starting compilation with args: "), + LinePattern("Compile on daemon: (\\d+) ms", { it.groups.get(1)?.value?.toLong()?.let { compileTime2 = it }; true }), + LinePattern("Shutdown complete")) + { unmatchedPattern, lineNo -> + TestCase.fail("pattern not found in the input: " + unmatchedPattern.regex + + "\nunmatched part of the log file (" + logFile.absolutePath + + ") from line " + lineNo + ":\n\n" + logFile.reader().useLines { it.drop(lineNo).joinToString("\n") }) + } } + TestCase.assertTrue("Expecting that compilation 1 ($compileTime1 ms) is at least two times longer than compilation 2 ($compileTime2 ms)", + compileTime1 > compileTime2 * 2) + logFile.delete() + run("hello.run", "-cp", jar, "Hello.HelloKt") + } + finally { + if (!daemonShotDown) + KotlinCompilerClient.shutdownCompileService(compilerId, daemonOptions) } - TestCase.assertTrue("Expecting that compilation 1 ($compileTime1 ms) is at least two times longer than compilation 2 ($compileTime2 ms)", - compileTime1 > compileTime2 * 2) - logFile.delete() - run("hello.run", "-cp", jar, "Hello.HelloKt") } finally { - if (!daemonShotDown) - KotlinCompilerClient.shutdownCompileService(compilerId, daemonOptions) + flagFile.delete() } } @@ -132,10 +136,9 @@ public class CompilerDaemonTest : KotlinIntegrationTestBase() { public fun testDaemonOptionsParsing() { val backupOptions = System.getProperty(COMPILE_DAEMON_OPTIONS_PROPERTY) try { - System.setProperty(COMPILE_DAEMON_OPTIONS_PROPERTY, "runFilesPath=abcd,clientAliveFlagPath=efgh,autoshutdownIdleSeconds=1111") + System.setProperty(COMPILE_DAEMON_OPTIONS_PROPERTY, "runFilesPath=abcd,autoshutdownIdleSeconds=1111") val opts = configureDaemonOptions() TestCase.assertEquals("abcd", opts.runFilesPath) - TestCase.assertEquals("efgh", opts.clientAliveFlagPath) TestCase.assertEquals(1111, opts.autoshutdownIdleSeconds) } finally { @@ -146,47 +149,48 @@ public class CompilerDaemonTest : KotlinIntegrationTestBase() { public fun testDaemonInstances() { val jar = tmpdir.absolutePath + File.separator + "hello1.jar" val flagFile = createTempFile(getTestName(true), ".alive") - flagFile.deleteOnExit() - val daemonOptions = DaemonOptions(runFilesPath = File(tmpdir, getTestName(true)).absolutePath, - clientAliveFlagPath = flagFile.absolutePath) - val compilerId2 = CompilerId.makeCompilerId(compilerClassPath + - File(KotlinIntegrationTestBase.getCompilerLib(), "kotlin-compiler-sources.jar")) + try { + val daemonOptions = DaemonOptions(runFilesPath = File(tmpdir, getTestName(true)).absolutePath) + val compilerId2 = CompilerId.makeCompilerId(compilerClassPath + + File(KotlinIntegrationTestBase.getCompilerLib(), "kotlin-compiler-sources.jar")) - KotlinCompilerClient.shutdownCompileService(compilerId, daemonOptions) - KotlinCompilerClient.shutdownCompileService(compilerId2, daemonOptions) + KotlinCompilerClient.shutdownCompileService(compilerId, daemonOptions) + KotlinCompilerClient.shutdownCompileService(compilerId2, daemonOptions) - val logFile1 = createTempFile("kotlin-daemon1-test", ".log") - val logFile2 = createTempFile("kotlin-daemon2-test", ".log") - val daemonJVMOptions1 = - configureDaemonJVMOptions("D$COMPILE_DAEMON_LOG_PATH_PROPERTY=\"${logFile1.absolutePath}\"", - inheritMemoryLimits = false, inheritAdditionalProperties = false) + val logFile1 = createTempFile("kotlin-daemon1-test", ".log") + val logFile2 = createTempFile("kotlin-daemon2-test", ".log") + val daemonJVMOptions1 = + configureDaemonJVMOptions("D$COMPILE_DAEMON_LOG_PATH_PROPERTY=\"${logFile1.absolutePath}\"", + inheritMemoryLimits = false, inheritAdditionalProperties = false) - val daemonJVMOptions2 = - configureDaemonJVMOptions("D$COMPILE_DAEMON_LOG_PATH_PROPERTY=\"${logFile2.absolutePath}\"", - inheritMemoryLimits = false, inheritAdditionalProperties = false) + val daemonJVMOptions2 = + configureDaemonJVMOptions("D$COMPILE_DAEMON_LOG_PATH_PROPERTY=\"${logFile2.absolutePath}\"", + inheritMemoryLimits = false, inheritAdditionalProperties = false) - TestCase.assertTrue(logFile1.length() == 0L && logFile2.length() == 0L) + TestCase.assertTrue(logFile1.length() == 0L && logFile2.length() == 0L) - val res1 = compileOnDaemon(compilerId, daemonJVMOptions1, daemonOptions, - "-include-runtime", File(getHelloAppBaseDir(), "hello.kt").absolutePath, "-d", jar) - TestCase.assertEquals("first compilation failed:\n${res1.out}", 0, res1.resultCode) + val res1 = compileOnDaemon(flagFile, compilerId, daemonJVMOptions1, daemonOptions, "-include-runtime") + TestCase.assertEquals("first compilation failed:\n${res1.out}", 0, res1.resultCode) - logFile1.assertLogContainsSequence("Starting compilation with args: ") - TestCase.assertEquals("expecting '${logFile2.absolutePath}' to be empty", 0L, logFile2.length()) + logFile1.assertLogContainsSequence("Starting compilation with args: ") + TestCase.assertEquals("expecting '${logFile2.absolutePath}' to be empty", 0L, logFile2.length()) - val res2 = compileOnDaemon(compilerId2, daemonJVMOptions2, daemonOptions, - "-include-runtime", File(getHelloAppBaseDir(), "hello.kt").absolutePath, "-d", jar) - TestCase.assertEquals("second compilation failed:\n${res2.out}", 0, res1.resultCode) + val res2 = compileOnDaemon(flagFile, compilerId2, daemonJVMOptions2, daemonOptions, "-include-runtime") + TestCase.assertEquals("second compilation failed:\n${res2.out}", 0, res1.resultCode) - logFile2.assertLogContainsSequence("Starting compilation with args: ") + logFile2.assertLogContainsSequence("Starting compilation with args: ") - KotlinCompilerClient.shutdownCompileService(compilerId, daemonOptions) - logFile1.assertLogContainsSequence("Shutdown complete") - logFile1.delete() + KotlinCompilerClient.shutdownCompileService(compilerId, daemonOptions) + logFile1.assertLogContainsSequence("Shutdown complete") + logFile1.delete() - KotlinCompilerClient.shutdownCompileService(compilerId2, daemonOptions) - logFile2.assertLogContainsSequence("Shutdown complete") - logFile2.delete() + KotlinCompilerClient.shutdownCompileService(compilerId2, daemonOptions) + logFile2.assertLogContainsSequence("Shutdown complete") + logFile2.delete() + } + finally { + flagFile.delete() + } } @@ -203,7 +207,7 @@ public class CompilerDaemonTest : KotlinIntegrationTestBase() { public fun testDaemonExecutionViaIntermediateProcess() { val clientAliveFile = createTempFile("kotlin-daemon-transitive-run-test", ".run") val runFilesPath = File(tmpdir, getTestName(true)).absolutePath - val daemonOptions = DaemonOptions(runFilesPath = runFilesPath, clientAliveFlagPath = clientAliveFile.absolutePath) + val daemonOptions = DaemonOptions(runFilesPath = runFilesPath) val jar = tmpdir.absolutePath + File.separator + "hello.jar" val args = listOf( File(File(System.getProperty("java.home"), "bin"), "java").absolutePath, @@ -257,43 +261,49 @@ public class CompilerDaemonTest : KotlinIntegrationTestBase() { TestCase.assertTrue(PARALLEL_THREADS_TO_COMPILE <= LoopbackNetworkInterface.SERVER_SOCKET_BACKLOG_SIZE) val flagFile = createTempFile(getTestName(true), ".alive") - flagFile.deleteOnExit() - val daemonOptions = DaemonOptions(runFilesPath = File(tmpdir, getTestName(true)).absolutePath, clientAliveFlagPath = flagFile.absolutePath) - val daemonJVMOptions = configureDaemonJVMOptions(inheritMemoryLimits = false, inheritAdditionalProperties = false) - val daemon = KotlinCompilerClient.connectToCompileService(compilerId, daemonJVMOptions, daemonOptions, DaemonReportingTargets(out = System.err), autostart = true, checkId = true) - TestCase.assertNotNull("failed to connect daemon", daemon) + try { + val daemonOptions = DaemonOptions(runFilesPath = File(tmpdir, getTestName(true)).absolutePath) + val daemonJVMOptions = configureDaemonJVMOptions(inheritMemoryLimits = false, inheritAdditionalProperties = false) + val daemon = KotlinCompilerClient.connectToCompileService(compilerId, flagFile, daemonJVMOptions, daemonOptions, DaemonReportingTargets(out = System.err), autostart = true, checkId = true) + TestCase.assertNotNull("failed to connect daemon", daemon) - val (registry, port) = findPortAndCreateRegistry(10, 16384, 65535) - val tracer = SynchronizationTracer(CountDownLatch(1), CountDownLatch(PARALLEL_THREADS_TO_COMPILE), port) + val (registry, port) = findPortAndCreateRegistry(10, 16384, 65535) + val tracer = SynchronizationTracer(CountDownLatch(1), CountDownLatch(PARALLEL_THREADS_TO_COMPILE), port) - val resultCodes = arrayOfNulls(PARALLEL_THREADS_TO_COMPILE) - val localEndSignal = CountDownLatch(PARALLEL_THREADS_TO_COMPILE) - val outStreams = Array(PARALLEL_THREADS_TO_COMPILE, { ByteArrayOutputStream() }) + val resultCodes = arrayOfNulls(PARALLEL_THREADS_TO_COMPILE) + val localEndSignal = CountDownLatch(PARALLEL_THREADS_TO_COMPILE) + val outStreams = Array(PARALLEL_THREADS_TO_COMPILE, { ByteArrayOutputStream() }) - fun runCompile(threadNo: Int) = - thread { - val jar = tmpdir.absolutePath + File.separator + "hello.$threadNo.jar" - val res = KotlinCompilerClient.compile(daemon!!, - CompileService.TargetPlatform.JVM, - arrayOf("-include-runtime", File(getHelloAppBaseDir(), "hello.kt").absolutePath, "-d", jar), - outStreams[threadNo], - port = port, - operationsTracer = tracer as RemoteOperationsTracer) - synchronized(resultCodes) { - resultCodes[threadNo] = res - } - localEndSignal.countDown() + fun runCompile(threadNo: Int) = + thread { + val jar = tmpdir.absolutePath + File.separator + "hello.$threadNo.jar" + val res = KotlinCompilerClient.compile( + daemon!!, + CompileService.NO_SESSION, + CompileService.TargetPlatform.JVM, + arrayOf("-include-runtime", File(getHelloAppBaseDir(), "hello.kt").absolutePath, "-d", jar), + outStreams[threadNo], + port = port, + operationsTracer = tracer as RemoteOperationsTracer) + synchronized(resultCodes) { + resultCodes[threadNo] = res + } + localEndSignal.countDown() + } + + (1..PARALLEL_THREADS_TO_COMPILE).forEach { runCompile(it - 1) } + + tracer.startSignal.countDown() + val succeeded = tracer.doneSignal.await(PARALLEL_WAIT_TIMEOUT_S, TimeUnit.SECONDS) + TestCase.assertTrue("parallel compilation failed to complete in $PARALLEL_WAIT_TIMEOUT_S ms, ${tracer.doneSignal.count} unfinished threads", succeeded) + + localEndSignal.await(PARALLEL_WAIT_TIMEOUT_S, TimeUnit.SECONDS) + (1..PARALLEL_THREADS_TO_COMPILE).forEach { + TestCase.assertEquals("Compilation on thread $it failed:\n${outStreams[it - 1]}", 0, resultCodes[it - 1]) } - - (1..PARALLEL_THREADS_TO_COMPILE).forEach { runCompile(it-1) } - - tracer.startSignal.countDown() - val succeeded = tracer.doneSignal.await(PARALLEL_WAIT_TIMEOUT_S, TimeUnit.SECONDS) - TestCase.assertTrue("parallel compilation failed to complete in $PARALLEL_WAIT_TIMEOUT_S ms, ${tracer.doneSignal.count} unfinished threads", succeeded) - - localEndSignal.await(PARALLEL_WAIT_TIMEOUT_S, TimeUnit.SECONDS) - (1..PARALLEL_THREADS_TO_COMPILE).forEach { - TestCase.assertEquals("Compilation on thread $it failed:\n${outStreams[it-1]}", 0, resultCodes[it-1]) + } + finally { + flagFile.delete() } } } diff --git a/jps-plugin/src/org/jetbrains/kotlin/compilerRunner/KotlinCompilerRunner.kt b/jps-plugin/src/org/jetbrains/kotlin/compilerRunner/KotlinCompilerRunner.kt index f1c2a675c47..b723b1c66b6 100644 --- a/jps-plugin/src/org/jetbrains/kotlin/compilerRunner/KotlinCompilerRunner.kt +++ b/jps-plugin/src/org/jetbrains/kotlin/compilerRunner/KotlinCompilerRunner.kt @@ -132,7 +132,7 @@ public object KotlinCompilerRunner { } - internal class DaemonConnection(public val daemon: CompileService?) + internal class DaemonConnection(public val daemon: CompileService?, public val sessionId: Int = CompileService.NO_SESSION) internal object getDaemonConnection { private @Volatile var connection: DaemonConnection? = null @@ -153,9 +153,13 @@ public object KotlinCompilerRunner { val profiler = if (daemonOptions.reportPerf) WallAndThreadAndMemoryTotalProfiler(withGC = false) else DummyProfiler() profiler.withMeasure(null) { - connection = DaemonConnection( - KotlinCompilerClient.connectToCompileService(compilerId, daemonJVMOptions, daemonOptions, DaemonReportingTargets(null, daemonReportMessages), true, true) - ) + fun newFlagFile(): File { + val flagFile = File.createTempFile("kotlin-compiler-jps-session-", "-is-running") + flagFile.deleteOnExit() + return flagFile + } + val daemon = KotlinCompilerClient.connectToCompileService(compilerId, daemonJVMOptions, daemonOptions, DaemonReportingTargets(null, daemonReportMessages), true, true) + connection = DaemonConnection(daemon, daemon?.leaseCompileSession(newFlagFile().absolutePath)?.get() ?:CompileService.NO_SESSION) } for (msg in daemonReportMessages) { @@ -196,7 +200,7 @@ public object KotlinCompilerRunner { K2JS_COMPILER -> CompileService.TargetPlatform.JS else -> throw IllegalArgumentException("Unknown compiler type $compilerClassName") } - val res = KotlinCompilerClient.incrementalCompile(connection!!.daemon!!, targetPlatform, argsArray, services, compilerOut, daemonOut) + val res = KotlinCompilerClient.incrementalCompile(connection!!.daemon!!, connection.sessionId, targetPlatform, argsArray, services, compilerOut, daemonOut) processCompilerOutput(messageCollector, collector, compilerOut, res.toString()) BufferedReader(StringReader(daemonOut.toString())).forEachLine {