Replace reflection-based compiler arguments copying with generated code

Using of Kotlin reflection for simple operations like bean management is very slow

First time initialization time: 261 ms for `copyBean(K2JVMCompilerArguments())`
Subsequent calls of `copyBean(K2JVMCompilerArguments())` take 1.7 ms per call

Unfortunately compiler argument handling is also used in Kotlin IntelliJ plugin
to parse facet settings. Big projects may have thousands of Kotlin facets

The same `ArgumentUtilsKt.copyProperties` frame is seen across various freezes:
IDEA-252440 2-3 minutes freeze on Kotlin project reimporting in last 203 eap
IDEA-253107 A lot of short freezes (1-3 sec) during Kotlin project development
KTIJ-23501 Make main run configuration detection lighter
KTIJ-22435 Unresponsive UI with 100% cpu

Reflection issue:
KT-56358 KClasses.getMemberProperties takes too much time

This commit replaces all reflection stuff with a simple code generation
Now `K2JVMCompilerArguments().clone()` goes to hard-to-measure time
This commit is contained in:
Leonid Shalupov
2023-03-08 18:20:51 +01:00
parent 56557fb8ff
commit 7480befe32
34 changed files with 744 additions and 75 deletions
+3
View File
@@ -78,6 +78,7 @@ dependencies {
testImplementation(projectTests(":compiler:tests-common-new"))
testImplementation(projectTests(":js:js.tests"))
testImplementation(project(":kotlin-gradle-compiler-types"))
testImplementation(project(":jps:jps-common"))
testApiJUnit5()
if (Ide.IJ()) {
@@ -91,6 +92,8 @@ projectTest(parallel = true) {
workingDir = rootDir
}
val generateCompilerArgumentsCopy by generator("org.jetbrains.kotlin.generators.arguments.GenerateCompilerArgumentsCopyKt")
val generateTests by generator("org.jetbrains.kotlin.generators.tests.GenerateTestsKt") {
dependsOn(":generators:analysis-api-generator:generateFrontendApiTests")
}
@@ -0,0 +1,129 @@
/*
* Copyright 2010-2023 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.generators.arguments
import org.jetbrains.kotlin.cli.common.arguments.*
import org.jetbrains.kotlin.config.CompilerSettings
import org.jetbrains.kotlin.config.JpsPluginSettings
import org.jetbrains.kotlin.utils.Printer
import java.io.File
import kotlin.reflect.KClass
import kotlin.reflect.KClassifier
import kotlin.reflect.KType
import kotlin.reflect.full.declaredMemberProperties
import kotlin.reflect.full.superclasses
private val CLASSES_TO_PROCESS: List<KClass<*>> = listOf(
JpsPluginSettings::class,
CompilerSettings::class,
K2MetadataCompilerArguments::class,
K2NativeCompilerArguments::class,
K2JSDceArguments::class,
K2JSCompilerArguments::class,
K2JVMCompilerArguments::class,
)
private val PACKAGE_TO_DIR_MAPPING: Map<Package, File> = mapOf(
K2JVMCompilerArguments::class.java.`package` to File("compiler/cli/cli-common/gen"),
JpsPluginSettings::class.java.`package` to File("jps/jps-common/gen"),
)
fun generateCompilerArgumentsCopy(withPrinterToFile: (targetFile: File, Printer.() -> Unit) -> Unit) {
val processed = mutableSetOf<KClass<*>>()
for (klass in CLASSES_TO_PROCESS) {
generateRec(klass, withPrinterToFile, processed)
}
}
private fun generateRec(
klass: KClass<*>,
withPrinterToFile: (targetFile: File, Printer.() -> Unit) -> Unit,
processed: MutableSet<KClass<*>>,
) {
if (!processed.add(klass)) return
val klassName = klass.simpleName!!
val fqn = klass.qualifiedName!!
val `package` = klass.java.`package`
val destDir = PACKAGE_TO_DIR_MAPPING[`package`]!!.resolve(`package`.name.replace('.', '/'))
withPrinterToFile(destDir.resolve(klassName + "CopyGenerated.kt")) {
println(
"""
@file:Suppress("unused", "DuplicatedCode")
// DO NOT EDIT MANUALLY!
// Generated by generators/tests/org/jetbrains/kotlin/generators/arguments/GenerateCompilerArgumentsCopy.kt
// To regenerate run 'generateCompilerArgumentsCopy' task
package ${`package`.name}
""".trimIndent()
)
fun isSupportedImmutable(type: KType): Boolean {
val classifier: KClassifier = type.classifier!!
return when {
classifier is KClass<*> && classifier == List::class -> isSupportedImmutable(type.arguments.single().type!!)
classifier == InternalArgument::class -> true
classifier == Boolean::class -> true
classifier == Int::class -> true
classifier == String::class -> true
else -> false
}
}
println("@OptIn(org.jetbrains.kotlin.utils.IDEAPluginsCompatibilityAPI::class)")
println("fun copy$klassName(from: $klassName, to: $klassName): $klassName {")
withIndent {
val superClasses: List<KClass<*>> = klass.superclasses.filterNot { it.java.isInterface }
check(superClasses.size < 2) {
"too many super classes in $klass: ${superClasses.joinToString()}"
}
val superKlass = superClasses.singleOrNull()
if (superKlass != null && superKlass != Freezable::class) {
generateRec(superKlass, withPrinterToFile, processed)
if (superKlass.java.`package` != `package`) {
print("${superKlass.java.`package`.name}.")
}
println("copy${superKlass.simpleName}(from, to)")
println()
}
val properties = collectProperties(klass, false)
for (property in properties.filter { klass.declaredMemberProperties.contains(it) }) {
val type = property.returnType
val classifier: KClassifier = type.classifier!!
when {
// Please add cases on the go
// Please add a test to GenerateCompilerArgumentsCopyTest if the change is not trivial
classifier is KClass<*> && classifier.java.isArray -> {
val arrayElementType = type.arguments.single().type!!
val nullableMarker = if (type.isMarkedNullable) "?" else ""
when (arrayElementType.classifier) {
String::class -> println("to.${property.name} = from.${property.name}${nullableMarker}.copyOf()")
else -> error("Unsupported array element type $arrayElementType (member '${property.name}' of $fqn)")
}
}
isSupportedImmutable(type) -> println("to.${property.name} = from.${property.name}")
else -> error("Unsupported type to copy: $type (member '${property.name}' of $fqn)")
}
}
println()
println("return to")
}
println("}")
}
}
fun main() {
generateCompilerArgumentsCopy(::getPrinterToFile)
}
@@ -0,0 +1,54 @@
/*
* Copyright 2010-2023 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.generators.arguments
import junit.framework.TestCase
import org.jetbrains.kotlin.cli.common.arguments.*
import java.lang.reflect.Modifier
import kotlin.reflect.jvm.javaField
import kotlin.test.assertContentEquals
class GenerateCompilerArgumentsCopyTest : TestCase() {
fun testCompilerArgumentsCopyFunctionsAreUpToDate() {
generateCompilerArgumentsCopy(::getPrinterForTests)
}
fun testCopyDoesNotCopyTransientFields() {
val a = K2JVMCompilerArguments()
a.errors = ArgumentParseErrors()
a.moduleName = "my module name"
val b = K2JVMCompilerArguments()
assertNull(b.errors)
assertNull(b.moduleName)
copyK2JVMCompilerArguments(a, b)
assertNull(b.errors)
assertEquals("my module name", b.moduleName)
}
fun testCopyDuplicatesArray() {
val a = K2JVMCompilerArguments()
a.additionalJavaModules = arrayOf("xxx")
val b = K2JVMCompilerArguments()
copyK2JVMCompilerArguments(a, b)
assertContentEquals(a.additionalJavaModules, b.additionalJavaModules)
assertNotSame(a.additionalJavaModules, b.additionalJavaModules)
b.additionalJavaModules!![0] = "yyy"
assertEquals("xxx", a.additionalJavaModules!![0])
}
fun testCollectPropertiesDoesNotReturnTransient() {
val errorProperty = CommonToolArguments::errors
assertTrue(Modifier.isTransient(errorProperty.javaField!!.modifiers))
val properties = collectProperties(CommonToolArguments::class, false)
assertFalse(properties.any { it.name == errorProperty.name })
}
}
@@ -7,7 +7,6 @@ package org.jetbrains.kotlin.generators.arguments
import org.jetbrains.kotlin.utils.Printer
import java.io.File
import java.io.PrintStream
fun generateGradleCompilerTypes(withPrinterToFile: (targetFile: File, Printer.() -> Unit) -> Unit) {
val destDir = File("libraries/tools/kotlin-gradle-compiler-types/src/generated/kotlin")
@@ -27,16 +26,5 @@ fun generateGradleCompilerTypes(withPrinterToFile: (targetFile: File, Printer.()
}
fun main() {
fun getPrinter(file: File, fn: Printer.() -> Unit) {
if (!file.exists()) {
file.parentFile.mkdirs()
file.createNewFile()
}
PrintStream(file.outputStream().buffered()).use {
val printer = Printer(it)
printer.fn()
}
}
generateGradleCompilerTypes(::getPrinter)
generateGradleCompilerTypes(::getPrinterToFile)
}
@@ -11,7 +11,6 @@ import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.utils.Printer
import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstanceOrNull
import java.io.File
import java.io.PrintStream
import kotlin.reflect.KAnnotatedElement
import kotlin.reflect.KClass
import kotlin.reflect.KProperty1
@@ -145,18 +144,7 @@ fun generateKotlinGradleOptions(withPrinterToFile: (targetFile: File, Printer.()
}
fun main() {
fun getPrinter(file: File, fn: Printer.() -> Unit) {
if (!file.exists()) {
file.parentFile.mkdirs()
file.createNewFile()
}
PrintStream(file.outputStream()).use {
val printer = Printer(it)
printer.fn()
}
}
generateKotlinGradleOptions(::getPrinter)
generateKotlinGradleOptions(::getPrinterToFile)
}
private fun generateKotlinCommonToolOptions(
@@ -9,42 +9,14 @@ package org.jetbrains.kotlin.generators.arguments.test
import junit.framework.TestCase
import org.jetbrains.kotlin.generators.arguments.generateGradleCompilerTypes
import org.jetbrains.kotlin.generators.arguments.generateKotlinGradleOptions
import org.jetbrains.kotlin.test.testFramework.KtUsefulTestCase
import org.jetbrains.kotlin.utils.Printer
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.PrintStream
import org.jetbrains.kotlin.generators.arguments.getPrinterForTests
class GenerateKotlinGradleOptionsTest : TestCase() {
fun testKotlinGradleOptionsAreUpToDate() {
fun getPrinter(file: File, fn: Printer.() -> Unit) {
val bytesOut = ByteArrayOutputStream()
PrintStream(bytesOut).use {
val printer = Printer(it)
printer.fn()
}
val upToDateContent = bytesOut.toString()
KtUsefulTestCase.assertSameLinesWithFile(file.absolutePath, upToDateContent)
}
generateKotlinGradleOptions(::getPrinter)
generateKotlinGradleOptions(::getPrinterForTests)
}
fun testKotlinGradleTypesAreUpToDate() {
fun getPrinter(file: File, fn: Printer.() -> Unit) {
val bytesOut = ByteArrayOutputStream()
PrintStream(bytesOut).use {
val printer = Printer(it)
printer.fn()
}
val upToDateContent = bytesOut.toString()
KtUsefulTestCase.assertSameLinesWithFile(file.absolutePath, upToDateContent)
}
generateGradleCompilerTypes(::getPrinter)
generateGradleCompilerTypes(::getPrinterForTests)
}
}
@@ -0,0 +1,26 @@
/*
* Copyright 2010-2023 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.generators.arguments
import org.jetbrains.kotlin.test.testFramework.KtUsefulTestCase
import org.jetbrains.kotlin.utils.Printer
import java.io.File
import java.nio.file.Files
private fun printToString(fn: Printer.() -> Unit): String {
val builder = StringBuilder()
Printer(builder).fn()
return builder.toString()
}
fun getPrinterForTests(file: File, fn: Printer.() -> Unit) {
KtUsefulTestCase.assertSameLinesWithFile(file.absolutePath, printToString(fn))
}
fun getPrinterToFile(file: File, fn: Printer.() -> Unit) {
Files.createDirectories(file.toPath().parent)
file.writeText(printToString(fn))
}