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:
committed by
Yan Zhulanow
parent
4851a83a83
commit
069579fd79
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
+64
@@ -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?
|
||||
}
|
||||
+61
@@ -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)
|
||||
}
|
||||
}
|
||||
+24
@@ -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() {}
|
||||
}
|
||||
+44
@@ -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)
|
||||
+106
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
+29
-15
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user