Debugger: Disable code interpreting by default in evaluator

Provide the compiled bytecode to debugger instead of interpreting the expression by default.
This significantly increases evaluation speed in numerous cases, e.g. `(1..10000).map { it.toString() }.count()`.
Leave interpreter mode as a fallback.

Android does not provide decent in-memory class loading support.
Moreover, we need to run dex on the compiled classes, and it required significant time.
So for now compiling evaluator for Android is available only in Android O, and only if there're any local classes or non-inline lambdas (evaluator didn't work for them before so we may consider this change as an improvement).
This commit is contained in:
Yan Zhulanow
2017-04-19 02:17:28 +03:00
committed by Yan Zhulanow
parent 4851a83a83
commit 069579fd79
16 changed files with 626 additions and 105 deletions
+1
View File
@@ -24,5 +24,6 @@
<orderEntry type="library" name="uast-java" level="project" />
<orderEntry type="library" name="kotlin-reflect" level="project" />
<orderEntry type="module" module-name="light-classes" />
<orderEntry type="library" scope="PROVIDED" name="dx-android" level="project" />
</component>
</module>
@@ -0,0 +1,49 @@
/*
* Copyright 2010-2017 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.android.debugger
import com.android.dx.cf.direct.DirectClassFile
import com.android.dx.cf.direct.StdAttributeFactory
import com.android.dx.command.dexer.Main
import com.android.dx.dex.cf.CfTranslator
import com.android.dx.dex.file.DexFile
import org.jetbrains.kotlin.idea.debugger.evaluate.classLoading.ClassToLoad
class AndroidDexWrapper {
@Suppress("unused") // Used in AndroidOClassLoadingAdapter#dex
fun dex(classes: Collection<ClassToLoad>): ByteArray? {
val dexArguments = Main.Arguments().apply { parse(arrayOf("testArgs")) }
val dexFile = DexFile(dexArguments.dexOptions)
for ((_, relativeFileName, bytes) in classes) {
val cf = DirectClassFile(bytes, relativeFileName, true)
cf.setAttributeFactory(StdAttributeFactory.THE_ONE)
val classDef = CfTranslator.translate(
cf,
bytes,
dexArguments.cfOptions,
dexArguments.dexOptions,
dexFile
)
dexFile.add(classDef)
}
return dexFile.toDex(null, false)
}
}
@@ -0,0 +1,72 @@
/*
* Copyright 2010-2017 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.android.debugger
import com.intellij.openapi.module.ModuleManager
import com.intellij.openapi.project.Project
import com.intellij.openapi.roots.ProjectRootModificationTracker
import com.intellij.psi.util.CachedValueProvider
import com.intellij.psi.util.CachedValuesManager
import org.jetbrains.android.facet.AndroidFacet
import org.jetbrains.kotlin.idea.debugger.evaluate.classLoading.AndroidDexer
import org.jetbrains.kotlin.idea.debugger.evaluate.classLoading.ClassToLoad
import java.io.File
import java.net.URLClassLoader
import java.security.ProtectionDomain
class AndroidDexerImpl(val project: Project) : AndroidDexer {
private val cachedDexWrapper = CachedValuesManager.getManager(project).createCachedValue({
val dexWrapper = doGetAndroidDexFile()?.let { dexJarFile ->
val androidDexWrapperName = AndroidDexWrapper::class.java.canonicalName
val classBytes = this.javaClass.classLoader.getResource(
androidDexWrapperName.replace('.', '/') + ".class").readBytes()
val dexClassLoader = object : URLClassLoader(arrayOf(dexJarFile.toURI().toURL()), this::class.java.classLoader) {
init {
defineClass(androidDexWrapperName, classBytes, 0, classBytes.size, null as ProtectionDomain?)
}
}
Class.forName(androidDexWrapperName, true, dexClassLoader).newInstance()
}
CachedValueProvider.Result.createSingleDependency(dexWrapper, ProjectRootModificationTracker.getInstance(project))
}, /* trackValue = */ false)
override fun dex(classes: Collection<ClassToLoad>): ByteArray? {
val dexWrapper = cachedDexWrapper.value
val dexMethod = dexWrapper::class.java.methods.firstOrNull { it.name == "dex" } ?: return null
return dexMethod.invoke(dexWrapper, classes) as? ByteArray ?: return null
}
private fun doGetAndroidDexFile(): File? {
for (module in ModuleManager.getInstance(project).modules) {
val androidFacet = AndroidFacet.getInstance(module) ?: continue
val sdkData = androidFacet.sdkData ?: continue
val latestBuildTool = sdkData.getLatestBuildTool(/* allowPreview = */ false)
?: sdkData.getLatestBuildTool(/* allowPreview = */ true)
?: continue
val dxJar = File(latestBuildTool.location, "lib/dx.jar")
if (dxJar.exists()) {
return dxJar
}
}
return null
}
}
+2
View File
@@ -84,6 +84,8 @@
<projectConfigurator implementation="org.jetbrains.kotlin.android.configure.KotlinAndroidGradleModuleConfigurator"/>
<platformGradleDetector implementation="org.jetbrains.kotlin.android.configure.PlatformAndroidGradleDetector"/>
<completionInformationProvider implementation="org.jetbrains.kotlin.AndroidExtensionsCompletionInformationProvider" />
<androidDexer implementation="org.jetbrains.kotlin.android.debugger.AndroidDexerImpl"/>
</extensions>
<project-components>
+4
View File
@@ -6,6 +6,10 @@
<extensionPoint name="declarationAttributeAltererExtension"
interface="org.jetbrains.kotlin.extensions.DeclarationAttributeAltererExtension"
area="IDEA_PROJECT"/>
<extensionPoint name="androidDexer"
interface="org.jetbrains.kotlin.idea.debugger.evaluate.classLoading.AndroidDexer"
area="IDEA_PROJECT"/>
</extensionPoints>
<extensions defaultExtensionNs="org.jetbrains.kotlin">
@@ -42,6 +42,7 @@ import org.jetbrains.kotlin.idea.caches.resolve.analyzeFullyAndGetResult
import org.jetbrains.kotlin.idea.debugger.BinaryCacheKey
import org.jetbrains.kotlin.idea.debugger.BytecodeDebugInfo
import org.jetbrains.kotlin.idea.debugger.WeakBytecodeDebugInfoStorage
import org.jetbrains.kotlin.idea.debugger.evaluate.classLoading.ClassToLoad
import org.jetbrains.kotlin.idea.runInReadActionWithWriteActionPriority
import org.jetbrains.kotlin.idea.util.application.runReadAction
import org.jetbrains.kotlin.psi.KtCodeFragment
@@ -217,8 +218,7 @@ class KotlinDebuggerCaches(project: Project) {
}
data class CompiledDataDescriptor(
val bytecodes: ByteArray,
val additionalClasses: List<Pair<String, ByteArray>>,
val classes: List<ClassToLoad>,
val sourcePosition: SourcePosition,
val parameters: ParametersDescriptor
)
@@ -61,7 +61,9 @@ import org.jetbrains.kotlin.idea.core.quoteSegmentsIfNeeded
import org.jetbrains.kotlin.idea.debugger.DebuggerUtils
import org.jetbrains.kotlin.idea.debugger.evaluate.KotlinDebuggerCaches.CompiledDataDescriptor
import org.jetbrains.kotlin.idea.debugger.evaluate.KotlinDebuggerCaches.ParametersDescriptor
import org.jetbrains.kotlin.idea.debugger.evaluate.classLoading.ClassToLoad
import org.jetbrains.kotlin.idea.debugger.evaluate.compilingEvaluator.loadClasses
import org.jetbrains.kotlin.idea.debugger.evaluate.compilingEvaluator.loadClassesSafely
import org.jetbrains.kotlin.idea.refactoring.introduce.extractionEngine.ExtractionResult
import org.jetbrains.kotlin.idea.runInReadActionWithWriteActionPriority
import org.jetbrains.kotlin.idea.util.application.runReadAction
@@ -78,6 +80,7 @@ import org.jetbrains.kotlin.resolve.jvm.JvmClassName
import org.jetbrains.org.objectweb.asm.*
import org.jetbrains.org.objectweb.asm.Opcodes.ASM5
import org.jetbrains.org.objectweb.asm.Type
import org.jetbrains.org.objectweb.asm.tree.ClassNode
import org.jetbrains.org.objectweb.asm.tree.MethodNode
import java.util.*
@@ -134,14 +137,37 @@ class KotlinEvaluator(val codeFragment: KtCodeFragment, val sourcePosition: Sour
isCompiledDataFromCache = false
extractAndCompile(fragment, position, context)
}
val result = runEval4j(context, compiledData)
val classLoaderHandler = loadClassesSafely(context, compiledData.classes)
val result = if (classLoaderHandler != null) {
try {
evaluateWithCompilation(context, compiledData) ?: runEval4j(context, compiledData)
} finally {
classLoaderHandler.dispose()
}
}
else {
runEval4j(context, compiledData)
}
// If bytecode was taken from cache and exception was thrown - recompile bytecode and run eval4j again
if (isCompiledDataFromCache && result is ExceptionThrown && result.kind == ExceptionThrown.ExceptionKind.BROKEN_CODE) {
return runEval4j(context, extractAndCompile(codeFragment, sourcePosition, context)).toJdiValue(context)
// We need only lambda classes here cause we using only eval4j evaluation method
val classLoaderHandler = loadClasses(context, compiledData.classes.drop(1))
try {
return runEval4j(context, extractAndCompile(codeFragment, sourcePosition, context)).toJdiValue(context)
} finally {
classLoaderHandler.dispose()
}
}
return result.toJdiValue(context)
return if (result is InterpreterResult) {
result.toJdiValue(context)
} else {
result
}
}
catch(e: EvaluateException) {
throw e
@@ -217,10 +243,9 @@ class KotlinEvaluator(val codeFragment: KtCodeFragment, val sourcePosition: Sour
}
}
val additionalFiles = outputFiles.drop(1).map { getClassName(it.relativePath) to it.asByteArray() }
val additionalFiles = outputFiles.map { ClassToLoad(getClassName(it.relativePath), it.relativePath, it.asByteArray()) }
return CompiledDataDescriptor(
outputFiles.first().asByteArray(),
additionalFiles,
sourcePosition,
parametersDescriptor)
@@ -230,15 +255,66 @@ class KotlinEvaluator(val codeFragment: KtCodeFragment, val sourcePosition: Sour
return fileName.substringBeforeLast(".class").replace("/", ".")
}
private fun evaluateWithCompilation(context: EvaluationContextImpl, compiledData: CompiledDataDescriptor): Any? {
val vm = context.debugProcess.virtualMachineProxy.virtualMachine
val classLoader = context.classLoader ?: return null
val mainClassBytecode = compiledData.classes[0].bytes
try {
val mainClassAsmNode = ClassNode().apply { ClassReader(mainClassBytecode).accept(this, ClassReader.EXPAND_FRAMES) }
val mainClassJdiName = mainClassAsmNode.name.replace('/', '.')
assert(mainClassAsmNode.methods.size == 1)
val methodToInvoke = mainClassAsmNode.methods[0]
assert(methodToInvoke.parameters == null || methodToInvoke.parameters.isEmpty())
val mainClass = classByName(context, mainClassJdiName, classLoader).reflectedType() as ClassType
val thread = context.suspendContext.thread?.threadReference!!
val invokePolicy = context.suspendContext.getInvokePolicy()
val eval = JDIEval(vm, classLoader, thread, invokePolicy)
return vm.executeWithBreakpointsDisabled {
// Prepare the main class
eval.loadClass(Type.getObjectType(mainClassAsmNode.name), classLoader)
val argumentTypes = Type.getArgumentTypes(methodToInvoke.desc)
val args = context.getArgumentsForEval4j(compiledData.parameters, argumentTypes)
.zip(argumentTypes)
.map { (value, type) ->
// Make argument type classes prepared for sure
eval.loadClass(type, classLoader)
boxOrUnboxArgumentIfNeeded(eval, value, type).asJdiValue(vm, type)
}
mainClass.invokeMethod(thread, mainClass.methods().single(), args, invokePolicy)
}
} catch (e: Throwable) {
LOG.debug("Unable to evaluate expression with compilation", e)
return null
}
}
private fun classByName(context: EvaluationContextImpl, className: String, classLoader: ClassLoaderReference): ClassObjectReference {
val process = context.debugProcess
val vm = process.virtualMachineProxy
val classClass = process.findClass(context, Class::class.java.canonicalName, classLoader) as ClassType
val forNameMethod = classClass.concreteMethodByName(
"forName", "(Ljava/lang/String;ZLjava/lang/ClassLoader;)Ljava/lang/Class;") ?: error("'forName' method not found")
return process.invokeMethod(context, classClass, forNameMethod, listOf(
vm.mirrorOf(className),
vm.mirrorOf(true),
classLoader)) as ClassObjectReference
}
private fun runEval4j(context: EvaluationContextImpl, compiledData: CompiledDataDescriptor): InterpreterResult {
val virtualMachine = context.debugProcess.virtualMachineProxy.virtualMachine
if (compiledData.additionalClasses.isNotEmpty()) {
loadClasses(context, compiledData.additionalClasses)
}
var resultValue: InterpreterResult? = null
ClassReader(compiledData.bytecodes).accept(object : ClassVisitor(ASM5) {
val mainClassBytecode = compiledData.classes[0].bytes
ClassReader(mainClassBytecode).accept(object : ClassVisitor(ASM5) {
override fun visitMethod(access: Int, name: String, desc: String, signature: String?, exceptions: Array<out String>?): MethodVisitor? {
if (name == GENERATED_FUNCTION_NAME) {
val argumentTypes = Type.getArgumentTypes(desc)
@@ -246,22 +322,18 @@ class KotlinEvaluator(val codeFragment: KtCodeFragment, val sourcePosition: Sour
return object : MethodNode(Opcodes.ASM5, access, name, desc, signature, exceptions) {
override fun visitEnd() {
val allRequests = virtualMachine.eventRequestManager().breakpointRequests() +
virtualMachine.eventRequestManager().classPrepareRequests()
allRequests.forEach { it.disable() }
virtualMachine.executeWithBreakpointsDisabled {
val eval = JDIEval(virtualMachine,
context.classLoader,
context.suspendContext.thread?.threadReference!!,
context.suspendContext.getInvokePolicy())
val eval = JDIEval(virtualMachine,
context.classLoader,
context.suspendContext.thread?.threadReference!!,
context.suspendContext.getInvokePolicy())
resultValue = interpreterLoop(
this,
makeInitialFrame(this, args.zip(argumentTypes).map { boxOrUnboxArgumentIfNeeded(eval, it.first, it.second) }),
eval
)
allRequests.forEach { it.enable() }
resultValue = interpreterLoop(
this,
makeInitialFrame(this, args.zip(argumentTypes).map { boxOrUnboxArgumentIfNeeded(eval, it.first, it.second) }),
eval
)
}
}
}
}
@@ -273,6 +345,17 @@ class KotlinEvaluator(val codeFragment: KtCodeFragment, val sourcePosition: Sour
return resultValue ?: throw IllegalStateException("resultValue is null: cannot find method " + GENERATED_FUNCTION_NAME)
}
private inline fun <T> VirtualMachine.executeWithBreakpointsDisabled(block: () -> T): T {
val allRequests = eventRequestManager().breakpointRequests() + eventRequestManager().classPrepareRequests()
try {
allRequests.forEach { it.disable() }
return block()
} finally {
allRequests.forEach { it.enable() }
}
}
private fun boxOrUnboxArgumentIfNeeded(eval: JDIEval, argumentValue: Value, parameterType: Type): Value {
val argumentType = argumentValue.asmType
@@ -541,7 +624,7 @@ private fun PsiElement.createKtFile(fileName: String, fileText: String): KtFile
return jetFile
}
private fun SuspendContext.getInvokePolicy(): Int {
internal fun SuspendContext.getInvokePolicy(): Int {
return if (suspendPolicy == EventRequest.SUSPEND_EVENT_THREAD) ObjectReference.INVOKE_SINGLE_THREADED else 0
}
@@ -0,0 +1,64 @@
/*
* Copyright 2010-2017 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.idea.debugger.evaluate.classLoading
import com.intellij.debugger.engine.DebugProcessImpl
import com.intellij.debugger.engine.evaluation.EvaluationContext
import com.intellij.debugger.engine.evaluation.EvaluationContextImpl
import com.intellij.debugger.impl.DebuggerUtilsEx
import com.sun.jdi.*
abstract class AbstractAndroidClassLoadingAdapter : ClassLoadingAdapter {
protected fun dex(context: EvaluationContextImpl, classes: Collection<ClassToLoad>): ByteArray? {
return AndroidDexer.getInstances(context.project).single().dex(classes)
}
protected fun mirrorOfByteArray(bytes: ByteArray, context: EvaluationContextImpl, process: DebugProcessImpl): ArrayReference {
val arrayClass = process.findClass(context, "byte[]", context.classLoader) as ArrayType
val reference = process.newInstance(arrayClass, bytes.size)
DebuggerUtilsEx.keep(reference, context)
for (i in 0..bytes.lastIndex) {
reference.setValue(i, process.virtualMachineProxy.mirrorOf(bytes[i]))
}
return reference
}
protected fun wrapToByteBuffer(bytes: ArrayReference, context: EvaluationContext, process: DebugProcessImpl): ObjectReference {
val byteBufferClass = process.findClass(context, "java.nio.ByteBuffer", context.classLoader) as ClassType
val wrapMethod = byteBufferClass.concreteMethodByName("wrap", "([B)Ljava/nio/ByteBuffer;")
?: error("'wrap' method not found")
return process.invokeMethod(context, byteBufferClass, wrapMethod, listOf(bytes)) as ObjectReference
}
protected fun DebugProcessImpl.tryLoadClass(
context: EvaluationContextImpl,
fqName: String,
classLoader: ClassLoaderReference?
): ReferenceType? {
return try {
loadClass(context, fqName, classLoader)
} catch (e: Throwable) {
null
}
}
}
internal val ClassLoaderReference.isDalvikClassLoader: Boolean
get() = referenceType()?.name()?.startsWith("dalvik.system.") ?: false
@@ -0,0 +1,26 @@
/*
* Copyright 2010-2017 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.idea.debugger.evaluate.classLoading
import org.jetbrains.kotlin.extensions.ProjectExtensionDescriptor
interface AndroidDexer {
companion object : ProjectExtensionDescriptor<AndroidDexer>(
"org.jetbrains.kotlin.androidDexer", AndroidDexer::class.java)
fun dex(classes: Collection<ClassToLoad>): ByteArray?
}
@@ -0,0 +1,61 @@
/*
* Copyright 2010-2017 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.idea.debugger.evaluate.classLoading
import com.intellij.debugger.engine.JVMNameUtil
import com.intellij.debugger.engine.evaluation.EvaluateException
import com.intellij.debugger.engine.evaluation.EvaluationContextImpl
import com.intellij.debugger.impl.DebuggerUtilsEx
import com.sun.jdi.*
class AndroidOClassLoadingAdapter : AbstractAndroidClassLoadingAdapter() {
override fun isApplicable(context: EvaluationContextImpl, classes: Collection<ClassToLoad>): Boolean {
if (classes.size <= 1) {
// Dex takes significant amount of time so we load classes only if we have some non-inline lambdas
return false
}
return context.classLoader?.isDalvikClassLoader ?: false
}
private fun resolveClassLoaderClass(context: EvaluationContextImpl): ClassType? {
try {
return context.debugProcess.tryLoadClass(
context, "dalvik.system.InMemoryDexClassLoader", context.classLoader) as? ClassType
} catch (e: EvaluateException) {
return null
}
}
override fun loadClasses(context: EvaluationContextImpl, classes: Collection<ClassToLoad>): ClassLoaderHandler {
val process = context.debugProcess
val inMemoryClassLoaderClass = resolveClassLoaderClass(context) ?: error("InMemoryDexClassLoader class not found")
val constructorMethod = inMemoryClassLoaderClass.concreteMethodByName(
JVMNameUtil.CONSTRUCTOR_NAME, "(Ljava/nio/ByteBuffer;Ljava/lang/ClassLoader;)V") ?: error("Constructor method not found")
val dexBytes = dex(context, classes) ?: error("Can't dex classes")
val dexBytesMirror = mirrorOfByteArray(dexBytes, context, process)
val dexByteBuffer = wrapToByteBuffer(dexBytesMirror, context, process)
val newClassLoader = process.newInstance(context, inMemoryClassLoaderClass, constructorMethod,
listOf(dexByteBuffer, context.classLoader))
DebuggerUtilsEx.keep(newClassLoader, context)
return ClassLoaderHandler(newClassLoader as ClassLoaderReference)
}
}
@@ -0,0 +1,24 @@
/*
* Copyright 2010-2017 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.idea.debugger.evaluate.classLoading
import com.intellij.openapi.Disposable
import com.sun.jdi.ClassLoaderReference
open class ClassLoaderHandler(val reference: ClassLoaderReference?) : Disposable {
override fun dispose() {}
}
@@ -0,0 +1,44 @@
/*
* Copyright 2010-2017 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.idea.debugger.evaluate.classLoading
import com.intellij.debugger.engine.evaluation.EvaluationContextImpl
import com.intellij.psi.PsiFile
import com.sun.jdi.ClassLoaderReference
interface ClassLoadingAdapter {
companion object {
private val ADAPTERS = listOf(
AndroidOClassLoadingAdapter(),
OrdinaryClassLoadingAdapter())
fun loadClasses(context: EvaluationContextImpl, classes: Collection<ClassToLoad>): ClassLoaderHandler {
for (adapter in ADAPTERS) {
if (adapter.isApplicable(context, classes)) {
return adapter.loadClasses(context, classes)
}
}
// Should never happen because OrdinaryClassLoadingAdapter is always applicable
throw IllegalStateException("Class loading adapter not found for $context")
}
}
fun isApplicable(context: EvaluationContextImpl, classes: Collection<ClassToLoad>): Boolean
fun loadClasses(context: EvaluationContextImpl, classes: Collection<ClassToLoad>): ClassLoaderHandler
}
@@ -0,0 +1,20 @@
/*
* Copyright 2010-2017 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.idea.debugger.evaluate.classLoading
@Suppress("ArrayInDataClass")
data class ClassToLoad(val className: String, val relativeFileName: String, val bytes: ByteArray)
@@ -0,0 +1,106 @@
/*
* Copyright 2010-2017 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.idea.debugger.evaluate.classLoading
import com.intellij.debugger.engine.DebugProcess
import com.intellij.debugger.engine.evaluation.EvaluateException
import com.intellij.debugger.engine.evaluation.EvaluationContext
import com.intellij.debugger.engine.evaluation.EvaluationContextImpl
import com.intellij.debugger.impl.ClassLoadingUtils
import com.intellij.openapi.projectRoots.JdkVersionUtil
import com.intellij.openapi.util.SystemInfo
import com.sun.jdi.ClassLoaderReference
import org.jetbrains.kotlin.idea.debugger.evaluate.CompilingEvaluatorUtils
class OrdinaryClassLoadingAdapter : ClassLoadingAdapter {
private companion object {
// This list should contain all superclasses of lambda classes.
// The order is relevant here: if we load Lambda first instead, during the definition of Lambda the class loader will try
// to load its superclass. It will succeed, probably with the help of some parent class loader, and the subsequent attempt to define
// the patched version of that superclass will fail with LinkageError (cannot redefine class)
private val LAMBDA_SUPERCLASSES = listOf(ClassBytes("kotlin.jvm.internal.Lambda"))
}
override fun isApplicable(context: EvaluationContextImpl, classes: Collection<ClassToLoad>): Boolean {
val classLoader = context.classLoader
return classLoader != null && !classLoader.isDalvikClassLoader
}
override fun loadClasses(context: EvaluationContextImpl, classes: Collection<ClassToLoad>): ClassLoaderHandler {
val process = context.debugProcess
val classLoader = try {
ClassLoadingUtils.getClassLoader(context, process)
}
catch (e: Exception) {
throw EvaluateException("Error creating evaluation class loader: " + e, e)
}
val version = process.virtualMachineProxy.version()
val sdkVersion = JdkVersionUtil.getVersion(version)
if (!SystemInfo.isJavaVersionAtLeast(sdkVersion.description)) {
throw EvaluateException(
"Unable to compile for target level ${sdkVersion.description}. " +
"Need to run IDEA on java version at least ${sdkVersion.description}, " +
"currently running on ${SystemInfo.JAVA_RUNTIME_VERSION}")
}
try {
defineClasses(classes, context, process, classLoader)
}
catch (e: Exception) {
throw EvaluateException("Error during classes definition " + e, e)
}
return ClassLoaderHandler(classLoader)
}
private fun defineClasses(
classes: Collection<ClassToLoad>,
context: EvaluationContext,
process: DebugProcess,
classLoader: ClassLoaderReference
) {
val classesToLoad = if (classes.size == 1) {
// No need in loading lambda superclass if there're no lambdas
classes
}
else {
val lambdaSuperclasses = LAMBDA_SUPERCLASSES.map {
ClassToLoad(it.name, it.name.replace('.', '/') + ".class", it.bytes)
}
lambdaSuperclasses + classes
}
for ((className, _, bytes) in classesToLoad) {
val patchedBytes = CompilingEvaluatorUtils.changeSuperToMagicAccessor(bytes)
ClassLoadingUtils.defineClass(className, patchedBytes, context, process, classLoader)
}
}
private class ClassBytes(val name: String) {
val bytes: ByteArray by lazy {
val inputStream = this::class.java.classLoader.getResourceAsStream(name.replace('.', '/') + ".class")
?: throw EvaluateException("Couldn't find $name class in current class loader")
inputStream.use {
it.readBytes()
}
}
}
}
@@ -16,72 +16,23 @@
package org.jetbrains.kotlin.idea.debugger.evaluate.compilingEvaluator
import com.intellij.debugger.engine.DebugProcess
import com.intellij.debugger.engine.evaluation.EvaluateException
import com.intellij.debugger.engine.evaluation.EvaluationContext
import com.intellij.debugger.engine.evaluation.EvaluationContextImpl
import com.intellij.debugger.impl.ClassLoadingUtils
import com.intellij.openapi.projectRoots.JdkVersionUtil
import com.intellij.openapi.util.SystemInfo
import com.sun.jdi.ClassLoaderReference
import org.jetbrains.kotlin.idea.debugger.evaluate.CompilingEvaluatorUtils
import org.jetbrains.kotlin.idea.debugger.evaluate.classLoading.ClassLoaderHandler
import org.jetbrains.kotlin.idea.debugger.evaluate.classLoading.ClassLoadingAdapter
import org.jetbrains.kotlin.idea.debugger.evaluate.classLoading.ClassToLoad
fun loadClasses(evaluationContext: EvaluationContextImpl, classes: Collection<Pair<String, ByteArray>>) {
val process = evaluationContext.debugProcess
val classLoader: ClassLoaderReference
fun loadClassesSafely(evaluationContext: EvaluationContextImpl, classes: Collection<ClassToLoad>): ClassLoaderHandler? {
try {
classLoader = ClassLoadingUtils.getClassLoader(evaluationContext, process)
}
catch (e: Exception) {
throw EvaluateException("Error creating evaluation class loader: " + e, e)
}
val version = (process.virtualMachineProxy).version()
val sdkVersion = JdkVersionUtil.getVersion(version)
if (!SystemInfo.isJavaVersionAtLeast(sdkVersion.description)) {
throw EvaluateException("Unable to compile for target level " + sdkVersion.description + ". Need to run IDEA on java version at least " + sdkVersion.description + ", currently running on " + SystemInfo.JAVA_RUNTIME_VERSION)
}
try {
defineClasses(classes, evaluationContext, process, classLoader)
}
catch (e: Exception) {
throw EvaluateException("Error during classes definition " + e, e)
}
evaluationContext.classLoader = classLoader
}
private fun defineClasses(
classes: Collection<Pair<String, ByteArray>>,
context: EvaluationContext,
process: DebugProcess,
classLoader: ClassLoaderReference
) {
val lambdaSuperclasses = LAMBDA_SUPERCLASSES.map { it.name to it.bytes }
for ((className, bytes) in lambdaSuperclasses + classes) {
val patchedBytes = CompilingEvaluatorUtils.changeSuperToMagicAccessor(bytes)
ClassLoadingUtils.defineClass(className, patchedBytes, context, process, classLoader)
return loadClasses(evaluationContext, classes)
} catch (e: Throwable) {
return null
}
}
// This list should contain all superclasses of lambda classes.
// The order is relevant here: if we load Lambda first instead, during the definition of Lambda the class loader will try
// to load its superclass. It will succeed, probably with the help of some parent class loader, and the subsequent attempt to define
// the patched version of that superclass will fail with LinkageError (cannot redefine class)
private val LAMBDA_SUPERCLASSES = listOf(
ClassBytes("kotlin.jvm.internal.Lambda")
)
fun loadClasses(evaluationContext: EvaluationContextImpl, classes: Collection<ClassToLoad>): ClassLoaderHandler {
if (classes.isEmpty()) return ClassLoaderHandler(evaluationContext.classLoader)
private class ClassBytes(val name: String) {
val bytes: ByteArray by lazy {
val inputStream = this::class.java.classLoader.getResourceAsStream(name.replace('.', '/') + ".class")
?: throw EvaluateException("Couldn't find $name class in current class loader")
inputStream.use {
it.readBytes()
}
return ClassLoadingAdapter.loadClasses(evaluationContext, classes).apply {
evaluationContext.classLoader = this.reference
}
}
}
@@ -23,6 +23,7 @@ import com.intellij.debugger.engine.evaluation.EvaluateException
import com.intellij.debugger.engine.evaluation.TextWithImports
import com.intellij.debugger.engine.evaluation.TextWithImportsImpl
import com.intellij.debugger.engine.evaluation.expression.EvaluatorBuilderImpl
import com.intellij.debugger.engine.events.SuspendContextCommandImpl
import com.intellij.debugger.settings.NodeRendererSettings
import com.intellij.debugger.ui.impl.watch.*
import com.intellij.debugger.ui.tree.*
@@ -454,26 +455,39 @@ abstract class AbstractKotlinEvaluateExpressionTest : KotlinDebuggerTestBase() {
contextElement.putCopyableUserData(KotlinCodeFragmentFactory.DEBUG_CONTEXT_FOR_TESTS, this@AbstractKotlinEvaluateExpressionTest.debuggerContext)
try {
val evaluator =
EvaluatorBuilderImpl.build(item,
contextElement,
sourcePosition,
project)
runActionInSuspendCommand {
try {
val evaluator = EvaluatorBuilderImpl.build(item, contextElement, sourcePosition, project)
?: throw AssertionError("Cannot create an Evaluator for Evaluate Expression")
if (evaluator == null) throw AssertionError("Cannot create an Evaluator for Evaluate Expression")
val value = evaluator.evaluate(this@AbstractKotlinEvaluateExpressionTest.evaluationContext)
val actualResult = value.asValue().asString()
if (expectedResult != null) {
Assert.assertTrue("Evaluate expression returns wrong result for ${item.text}:\nexpected = $expectedResult\nactual = $actualResult\n", expectedResult == actualResult)
val value = evaluator.evaluate(this@AbstractKotlinEvaluateExpressionTest.evaluationContext)
val actualResult = value.asValue().asString()
if (expectedResult != null) {
Assert.assertTrue("Evaluate expression returns wrong result for ${item.text}:" +
"\nexpected = $expectedResult\nactual = $actualResult\n", expectedResult == actualResult)
}
}
catch (e: EvaluateException) {
Assert.assertTrue("Evaluate expression throws wrong exception for ${item.text}:" +
"\nexpected = $expectedResult\nactual = ${e.message}\n", expectedResult == e.message?.replaceFirst(ID_PART_REGEX, "id=ID"))
}
}
catch (e: EvaluateException) {
Assert.assertTrue("Evaluate expression throws wrong exception for ${item.text}:\nexpected = $expectedResult\nactual = ${e.message}\n", expectedResult == e.message?.replaceFirst(ID_PART_REGEX, "id=ID"))
}
}
private fun SuspendContextImpl.runActionInSuspendCommand(action: SuspendContextImpl.() -> Unit) {
if (myInProgress) {
action()
} else {
val command = object : SuspendContextCommandImpl(this) {
override fun contextAction(suspendContext: SuspendContextImpl) {
action(suspendContext)
}
}
// Try to execute the action inside a command if we aren't already inside it.
debuggerContext.debugProcess?.managerThread?.invoke(command) ?: command.contextAction(this)
}
}