[CLI] Add cache for reflection lookup of CLI arguments

Add cache for reflection lookup of CLI arguments.

Replace CLI argument list with map.

The current cli parser tries to match every possible command line
argument against each command line argument, essentially in a double
loop. This fix replaces one of the loops with a map lookup. Building the 
map is not expensive, and pays for itself even with a modest number of
parameters. The map is cached between calls, making subsequent calls
much cheaper. If run in a daemon, repeatedly parsing, e.g., 250 
arguments, this speeds up argument parsing by a factor 20. 

Disallow -shortName=value in CLI arguments.

Co-authored-by: Troels Lund <troels@google.com>
This commit is contained in:
Troels Bjerre Lund
2023-12-19 17:05:03 +00:00
committed by Space Cloud
parent 357015f399
commit b668433256
@@ -21,6 +21,7 @@ import org.jetbrains.kotlin.konan.file.File
import org.jetbrains.kotlin.load.java.JvmAbi
import org.jetbrains.kotlin.utils.SmartList
import java.lang.reflect.Method
import java.util.concurrent.ConcurrentHashMap
import kotlin.reflect.KClass
import kotlin.reflect.cast
@@ -114,54 +115,38 @@ fun <A : CommonToolArguments> parseCommandLineArgumentsFromEnvironment(arguments
parseCommandLineArguments(settingsFromEnvironment, arguments, overrideArguments = true)
}
private data class ArgumentField(val getter: Method, val setter: Method, val argument: Argument)
private val argumentsCache = ConcurrentHashMap<Class<*>, Map<String, ArgumentField>>()
private fun getArguments(klass: Class<*>): Map<String, ArgumentField> = argumentsCache.getOrPut(klass) {
if (klass == Any::class.java) emptyMap()
else buildMap {
putAll(getArguments(klass.superclass))
for (field in klass.declaredFields) {
field.getAnnotation(Argument::class.java)?.let { argument ->
val getter = klass.getMethod(JvmAbi.getterName(field.name))
val setter = klass.getMethod(JvmAbi.setterName(field.name), field.type)
val argumentField = ArgumentField(getter, setter, argument)
for (key in listOf(argument.value, argument.shortName, argument.deprecatedName)) {
if (key.isNotEmpty()) put(key, argumentField)
}
}
}
}
}
private fun <A : CommonToolArguments> parsePreprocessedCommandLineArguments(
args: List<String>,
result: A,
errors: Lazy<ArgumentParseErrors>,
overrideArguments: Boolean
) {
data class ArgumentField(val getter: Method, val setter: Method, val argument: Argument)
val superClasses = mutableListOf<Class<*>>(result::class.java)
while (superClasses.last() != Any::class.java) {
superClasses.add(superClasses.last().superclass)
}
val resultClass = result::class.java
val properties = superClasses.flatMap {
it.declaredFields.mapNotNull { field ->
field.getAnnotation(Argument::class.java)?.let { argument ->
val getter = resultClass.getMethod(JvmAbi.getterName(field.name))
val setter = resultClass.getMethod(JvmAbi.setterName(field.name), field.type)
ArgumentField(getter, setter, argument)
}
}
}
val properties = getArguments(result::class.java)
val visitedArgs = mutableSetOf<String>()
var freeArgsStarted = false
fun ArgumentField.matches(arg: String): Boolean {
if (argument.shortName.takeUnless(String::isEmpty) == arg) {
return true
}
val deprecatedName = argument.deprecatedName
if (deprecatedName.isNotEmpty() && (deprecatedName == arg || arg.startsWith("$deprecatedName="))) {
errors.value.deprecatedArguments[deprecatedName] = argument.value
return true
}
if (argument.value == arg) {
if (argument.isAdvanced && getter.returnType.kotlin != Boolean::class) {
errors.value.extraArgumentsPassedInObsoleteForm.add(arg)
}
return true
}
return arg.startsWith(argument.value + "=")
}
val freeArgs = ArrayList<String>()
val internalArguments = ArrayList<InternalArgument>()
@@ -199,7 +184,8 @@ private fun <A : CommonToolArguments> parsePreprocessedCommandLineArguments(
continue
}
val argumentField = properties.firstOrNull { it.matches(arg) }
val key = arg.substringBefore('=')
val argumentField = properties[key]
if (argumentField == null) {
when {
arg.startsWith(ADVANCED_ARGUMENT_PREFIX) -> errors.value.unknownExtraFlags.add(arg)
@@ -210,6 +196,24 @@ private fun <A : CommonToolArguments> parsePreprocessedCommandLineArguments(
}
val (getter, setter, argument) = argumentField
// Tests for -shortName=value, which isn't currently allowed.
if (key != arg && key == argument.shortName) {
errors.value.unknownArgs.add(arg)
continue
}
val deprecatedName = argument.deprecatedName
if (deprecatedName == key) {
errors.value.deprecatedArguments[deprecatedName] = argument.value
}
if (argument.value == arg) {
if (argument.isAdvanced && getter.returnType.kotlin != Boolean::class) {
errors.value.extraArgumentsPassedInObsoleteForm.add(arg)
}
}
val value: Any = when {
getter.returnType.kotlin == Boolean::class -> {
if (arg.startsWith(argument.value + "=")) {