All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.jetbrains.kotlin.daemon.common.DaemonParams.kt Maven / Gradle / Ivy

There is a newer version: 2.1.20-Beta1
Show newest version
/*
 * Copyright 2010-2015 JetBrains s.r.o.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.jetbrains.kotlin.daemon.common

import org.jetbrains.kotlin.cli.common.CompilerSystemProperties
import java.io.File
import java.io.Serializable
import java.lang.management.ManagementFactory
import java.security.MessageDigest
import java.util.*
import kotlin.reflect.KMutableProperty1

const val COMPILER_JAR_NAME: String = "kotlin-compiler.jar"
const val COMPILER_SERVICE_RMI_NAME: String = "KotlinJvmCompilerService"
const val COMPILER_DAEMON_CLASS_FQN: String = "org.jetbrains.kotlin.daemon.KotlinCompileDaemon"
const val COMPILE_DAEMON_FIND_PORT_ATTEMPTS: Int = 10
const val COMPILE_DAEMON_PORTS_RANGE_START: Int = 17001
const val COMPILE_DAEMON_PORTS_RANGE_END: Int = 18000
const val COMPILE_DAEMON_CMDLINE_OPTIONS_PREFIX: String = "--daemon-"
const val COMPILE_DAEMON_DEFAULT_FILES_PREFIX: String = "kotlin-daemon"
const val COMPILE_DAEMON_TIMEOUT_INFINITE_S: Int = 0
const val COMPILE_DAEMON_DEFAULT_IDLE_TIMEOUT_S: Int = 7200 // 2 hours
const val COMPILE_DAEMON_DEFAULT_UNUSED_TIMEOUT_S: Int = 60
const val COMPILE_DAEMON_DEFAULT_SHUTDOWN_DELAY_MS: Long = 1000L // 1 sec
const val COMPILE_DAEMON_MEMORY_THRESHOLD_INFINITE: Long = 0L
const val COMPILE_DAEMON_FORCE_SHUTDOWN_DEFAULT_TIMEOUT_MS: Long = 10000L // 10 secs
const val COMPILE_DAEMON_TIMEOUT_INFINITE_MS: Long = 0L
const val COMPILE_DAEMON_IS_READY_MESSAGE = "Kotlin compile daemon is ready"

val COMPILE_DAEMON_DEFAULT_RUN_DIR_PATH: String
    get() = CompilerSystemProperties.COMPILE_DAEMON_CUSTOM_RUN_FILES_PATH_FOR_TESTS.value ?: FileSystem.getRuntimeStateFilesPath(
        "kotlin",
        "daemon"
    )

val CLASSPATH_ID_DIGEST = "MD5"


open class PropMapper>(
    val dest: C,
    val prop: P,
    val names: List = listOf(prop.name),
    val fromString: (String) -> V,
    val toString: ((V) -> String?) = { it.toString() },
    val skipIf: ((V) -> Boolean) = { false },
    val mergeDelimiter: String? = null
) {
    open fun toArgs(prefix: String = COMPILE_DAEMON_CMDLINE_OPTIONS_PREFIX): List =
        when {
            skipIf(prop.get(dest)) -> listOf()
            mergeDelimiter != null -> listOf(listOfNotNull(prefix + names.first(), toString(prop.get(dest))).joinToString(mergeDelimiter))
            else -> listOfNotNull(prefix + names.first(), toString(prop.get(dest)))
        }

    open fun apply(s: String) = prop.set(dest, fromString(s))
}


class NullablePropMapper>(dest: C,
                                                                       prop: P,
                                                                       names: List = listOf(),
                                                                       fromString: ((String) -> V),
                                                                       toString: ((V) -> String?) = { it.toString() },
                                                                       skipIf: ((V) -> Boolean) = { it == null },
                                                                       mergeDelimiter: String? = null)
: PropMapper(dest = dest, prop = prop, names = if (names.any()) names else listOf(prop.name),
                      fromString = fromString, toString = toString, skipIf = skipIf, mergeDelimiter = mergeDelimiter)


class StringPropMapper>(dest: C,
                                                                prop: P,
                                                                names: List = listOf(),
                                                                fromString: ((String) -> String) = { it },
                                                                toString: ((String) -> String?) = { it },
                                                                skipIf: ((String) -> Boolean) = String::isEmpty,
                                                                mergeDelimiter: String? = null)
: PropMapper(dest = dest, prop = prop, names = if (names.any()) names else listOf(prop.name),
                           fromString = fromString, toString = toString, skipIf = skipIf, mergeDelimiter = mergeDelimiter)


class BoolPropMapper>(dest: C, prop: P, names: List = listOf())
: PropMapper(dest = dest, prop = prop, names = if (names.any()) names else listOf(prop.name),
                            fromString = { true }, toString = { null }, skipIf = { !prop.get(dest) })


class RestPropMapper>>(dest: C, prop: P)
: PropMapper, P>(dest = dest, prop = prop, toString = { null }, fromString = { arrayListOf() }) {
    override fun toArgs(prefix: String): List = prop.get(dest).map { prefix + it }
    override fun apply(s: String) = add(s)
    fun add(s: String) {
        prop.get(dest).add(s)
    }
}


// helper function combining find with map, useful for the cases then there is a calculation performed in find, which is nice to return along with
// found value; mappingPredicate should return the pair of boolean compare predicate result and transformation value, we want to get along with found value
inline fun  Iterable.findWithTransform(mappingPredicate: (T) -> Pair): R? {
    for (element in this) {
        val (found, mapped) = mappingPredicate(element)
        if (found) return mapped
    }
    return null
}


// filter-like function, takes list of propmappers, bound to properties of concrete objects, iterates over receiver, extract matching values via appropriate
// mappers into bound properties; if restParser is given, adds all non-matching elements to it, otherwise return them as an iterable
// note bound properties mutation!
fun Iterable.filterExtractProps(propMappers: List>, prefix: String, restParser: RestPropMapper<*, *>? = null): Iterable {

    val iter = iterator()
    val rest = arrayListOf()

    while (iter.hasNext()) {
        val param = iter.next()
        val (propMapper, matchingOption) = propMappers.findWithTransform { mapper ->
            mapper.names
                    .firstOrNull { param.startsWith(prefix + it) }
                    .let { Pair(it != null, Pair(mapper, it)) }
        } ?: Pair(null, null)

        when {
            propMapper != null -> {
                val optionLength = prefix.length + matchingOption!!.length
                when {
                    propMapper is BoolPropMapper<*, *> -> {
                        if (param.length > optionLength)
                            throw IllegalArgumentException("Invalid switch option '$param', expecting $prefix$matchingOption without arguments")
                        propMapper.apply("")
                    }
                    param.length > optionLength ->
                        if (param[optionLength] != '=') {
                            if (propMapper.mergeDelimiter == null)
                                throw IllegalArgumentException("Invalid option syntax '$param', expecting $prefix$matchingOption[= ]")
                            propMapper.apply(param.substring(optionLength))
                        }
                        else {
                            propMapper.apply(param.substring(optionLength + 1))
                        }
                    else -> {
                        if (!iter.hasNext()) throw IllegalArgumentException("Expecting argument for the option $prefix$matchingOption")
                        propMapper.apply(iter.next())
                    }
                }
            }
            restParser != null && param.startsWith(prefix) ->
                restParser.add(param.removePrefix(prefix))
            else -> rest.add(param)
        }
    }
    return rest
}


fun String.trimQuotes() = trim('"','\'')


interface OptionsGroup : Serializable {
    val mappers: List>
}

fun Iterable.filterExtractProps(vararg groups: OptionsGroup, prefix: String): Iterable =
        filterExtractProps(groups.flatMap { it.mappers }, prefix)


data class DaemonJVMOptions(
        var maxMemory: String = "",
        var maxMetaspaceSize: String = "",
        var reservedCodeCacheSize: String = "320m",
        var jvmParams: MutableCollection = arrayListOf()
) : OptionsGroup {
    override val mappers: List>
        get() = listOf(StringPropMapper(this, DaemonJVMOptions::maxMemory, listOf("Xmx"), mergeDelimiter = ""),
                       StringPropMapper(this, DaemonJVMOptions::maxMetaspaceSize, listOf("XX:MaxMetaspaceSize"), mergeDelimiter = "="),
                       StringPropMapper(this, DaemonJVMOptions::reservedCodeCacheSize, listOf("XX:ReservedCodeCacheSize"), mergeDelimiter = "="),
                       restMapper)

    val restMapper: RestPropMapper<*, *>
        get() = RestPropMapper(this, DaemonJVMOptions::jvmParams)
}


data class DaemonOptions(
        var runFilesPath: String = COMPILE_DAEMON_DEFAULT_RUN_DIR_PATH,
        var autoshutdownMemoryThreshold: Long = COMPILE_DAEMON_MEMORY_THRESHOLD_INFINITE,
        var autoshutdownIdleSeconds: Int = COMPILE_DAEMON_DEFAULT_IDLE_TIMEOUT_S,
        var autoshutdownUnusedSeconds: Int = COMPILE_DAEMON_DEFAULT_UNUSED_TIMEOUT_S,
        var shutdownDelayMilliseconds: Long = COMPILE_DAEMON_DEFAULT_SHUTDOWN_DELAY_MS,
        var forceShutdownTimeoutMilliseconds: Long = COMPILE_DAEMON_FORCE_SHUTDOWN_DEFAULT_TIMEOUT_MS,
        var verbose: Boolean = false,
        var reportPerf: Boolean = false
) : OptionsGroup {

    override val mappers: List>
        get() = listOf(PropMapper(this, DaemonOptions::runFilesPath, fromString = String::trimQuotes),
                       PropMapper(this, DaemonOptions::autoshutdownMemoryThreshold, fromString = String::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 = String::toInt, skipIf = { it == 0 }, mergeDelimiter = "="),
                       PropMapper(this, DaemonOptions::autoshutdownUnusedSeconds, fromString = String::toInt, skipIf = { it == COMPILE_DAEMON_DEFAULT_UNUSED_TIMEOUT_S }, mergeDelimiter = "="),
                       PropMapper(this, DaemonOptions::shutdownDelayMilliseconds, fromString = String::toLong, skipIf = { it == COMPILE_DAEMON_DEFAULT_SHUTDOWN_DELAY_MS }, mergeDelimiter = "="),
                       PropMapper(this, DaemonOptions::forceShutdownTimeoutMilliseconds, fromString = String::toLong, skipIf = { it == COMPILE_DAEMON_FORCE_SHUTDOWN_DEFAULT_TIMEOUT_MS }, mergeDelimiter = "="),
                       BoolPropMapper(this, DaemonOptions::verbose),
                       BoolPropMapper(this, DaemonOptions::reportPerf))
}

// TODO: consider implementing generic approach to it or may be replace getters with ones returning default if necessary
val DaemonOptions.runFilesPathOrDefault: String
    get() = if (runFilesPath.isBlank()) COMPILE_DAEMON_DEFAULT_RUN_DIR_PATH else runFilesPath

fun Iterable.distinctStringsDigest(): ByteArray =
        MessageDigest.getInstance(CLASSPATH_ID_DIGEST)
                .digest(this.distinct().sorted().joinToString("").toByteArray())

fun ByteArray.toHexString(): String = joinToString("", transform = { "%02x".format(it) })


data class CompilerId(
        var compilerClasspath: List = listOf(),
        var compilerVersion: String = ""
) : OptionsGroup {

    override val mappers: List>
        get() = listOf(PropMapper(this, CompilerId::compilerClasspath, toString = { it.joinToString(File.pathSeparator) }, fromString = { it.trimQuotes().split(File.pathSeparator) }),
                       StringPropMapper(this, CompilerId::compilerVersion))

    fun digest(): String = compilerClasspath
        .map { File(it).absolutePath }
        .distinctStringsDigest()
        .toHexString()

    companion object {
        @JvmStatic
        fun makeCompilerId(vararg paths: File): CompilerId = makeCompilerId(paths.asIterable())

        @JvmStatic
        fun makeCompilerId(paths: Iterable): CompilerId =
                CompilerId(compilerClasspath = paths.map { it.absolutePath })
    }
}


fun isDaemonEnabled(): Boolean = CompilerSystemProperties.COMPILE_DAEMON_ENABLED_PROPERTY.value != null

fun configureDaemonJVMOptions(opts: DaemonJVMOptions,
                              vararg additionalParams: String,
                              inheritMemoryLimits: Boolean,
                              inheritOtherJvmOptions: Boolean,
                              inheritAdditionalProperties: Boolean
): DaemonJVMOptions =
        configureDaemonJVMOptions(opts, additionalParams.asIterable(), inheritMemoryLimits, inheritOtherJvmOptions, inheritAdditionalProperties)

// TODO: expose sources for testability and test properly
fun configureDaemonJVMOptions(opts: DaemonJVMOptions,
                              additionalParams: Iterable,
                              inheritMemoryLimits: Boolean,
                              inheritOtherJvmOptions: Boolean,
                              inheritAdditionalProperties: Boolean
): DaemonJVMOptions {
    // note: sequence matters, e.g. explicit override in COMPILE_DAEMON_JVM_OPTIONS_PROPERTY should be done after inputArguments processing
    if (inheritMemoryLimits || inheritOtherJvmOptions) {
        val jvmArguments = ManagementFactory.getRuntimeMXBean().inputArguments
        val targetOptions = if (inheritMemoryLimits) opts else DaemonJVMOptions()
        val otherArgs = jvmArguments.filterExtractProps(targetOptions.mappers, prefix = "-")

        if (inheritMemoryLimits) {
            if (opts.maxMemory.isBlank()) {
                val maxMemBytes = Runtime.getRuntime().maxMemory()
                // rounding up
                val maxMemMegabytes = maxMemBytes / (1024 * 1024) + if (maxMemBytes % (1024 * 1024) == 0L) 0 else 1
                opts.maxMemory = "${maxMemMegabytes}m"
            }
        }

        if (inheritOtherJvmOptions) {
            opts.jvmParams.addAll(
                otherArgs.filterNot {
                    it.startsWith("agentlib") ||
                            it.startsWith("D" + CompilerSystemProperties.COMPILE_DAEMON_LOG_PATH_PROPERTY.property) ||
                            it.startsWith("D" + CompilerSystemProperties.KOTLIN_COMPILER_ENVIRONMENT_KEEPALIVE_PROPERTY.property) ||
                            it.startsWith("D" + CompilerSystemProperties.COMPILE_DAEMON_JVM_OPTIONS_PROPERTY.property) ||
                            it.startsWith("D" + CompilerSystemProperties.COMPILE_DAEMON_OPTIONS_PROPERTY.property)
                })
        }
    }
    CompilerSystemProperties.COMPILE_DAEMON_JVM_OPTIONS_PROPERTY.value?.let {
        opts.jvmParams.addAll(
                it.trimQuotes()
                  .split("(?
                match[1]?.value?.let {
                    it.toLong() *
                    when (match[2]?.value) {
                        "k" -> 1 shl 10
                        "m" -> 1 shl 20
                        "g" -> 1 shl 30
                        else -> 1
                    }
                }
            }


private val daemonJVMOptionsMemoryProps =
    listOf(DaemonJVMOptions::maxMemory, DaemonJVMOptions::maxMetaspaceSize, DaemonJVMOptions::reservedCodeCacheSize)

infix fun DaemonJVMOptions.memorywiseFitsInto(other: DaemonJVMOptions): Boolean =
        daemonJVMOptionsMemoryProps
            .all { (it.get(this).memToBytes() ?: 0) <= (it.get(other).memToBytes() ?: 0) }

fun compareDaemonJVMOptionsMemory(left: DaemonJVMOptions, right: DaemonJVMOptions): Int {
    val props = daemonJVMOptionsMemoryProps.map { Pair(it.get(left).memToBytes() ?: 0, it.get(right).memToBytes() ?: 0) }
    return when {
        props.all { it.first == it.second } -> 0
        props.all { it.first <= it.second } -> -1
        props.all { it.first >= it.second } -> 1
        else -> 0
    }
}

class DaemonJVMOptionsMemoryComparator : Comparator {
    override fun compare(left: DaemonJVMOptions, right: DaemonJVMOptions): Int = compareDaemonJVMOptionsMemory(left, right)
}


fun DaemonJVMOptions.updateMemoryUpperBounds(other: DaemonJVMOptions): DaemonJVMOptions {
    daemonJVMOptionsMemoryProps
        .forEach { if ((it.get(this).memToBytes() ?: 0) < (it.get(other).memToBytes() ?: 0)) it.set(this, it.get(other)) }
    return this
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy