From 069579fd79cd7813d0586fa365ee5bff8afaf1cf Mon Sep 17 00:00:00 2001 From: Yan Zhulanow Date: Wed, 19 Apr 2017 02:17:28 +0300 Subject: [PATCH] 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). --- idea/idea-android/idea-android.iml | 1 + .../android/debugger/AndroidDexWrapper.kt | 49 +++++++ .../android/debugger/AndroidDexerImpl.kt | 72 +++++++++ idea/src/META-INF/android.xml | 2 + idea/src/META-INF/extensions/ide.xml | 4 + .../debugger/evaluate/KotlinDebuggerCaches.kt | 4 +- .../evaluate/KotlinEvaluationBuilder.kt | 137 ++++++++++++++---- .../AbstractAndroidClassLoadingAdapter.kt | 64 ++++++++ .../evaluate/classLoading/AndroidDexer.kt | 26 ++++ .../AndroidOClassLoadingAdapter.kt | 61 ++++++++ .../classLoading/ClassLoaderHandler.kt | 24 +++ .../classLoading/ClassLoadingAdapter.kt | 44 ++++++ .../evaluate/classLoading/ClassToLoad.kt | 20 +++ .../OrdinaryClassLoadingAdapter.kt | 106 ++++++++++++++ .../debugger/evaluate/compilingEvaluator.kt | 73 ++-------- .../AbstractKotlinEvaluateExpressionTest.kt | 44 ++++-- 16 files changed, 626 insertions(+), 105 deletions(-) create mode 100644 idea/idea-android/src/org/jetbrains/kotlin/android/debugger/AndroidDexWrapper.kt create mode 100644 idea/idea-android/src/org/jetbrains/kotlin/android/debugger/AndroidDexerImpl.kt create mode 100644 idea/src/org/jetbrains/kotlin/idea/debugger/evaluate/classLoading/AbstractAndroidClassLoadingAdapter.kt create mode 100644 idea/src/org/jetbrains/kotlin/idea/debugger/evaluate/classLoading/AndroidDexer.kt create mode 100644 idea/src/org/jetbrains/kotlin/idea/debugger/evaluate/classLoading/AndroidOClassLoadingAdapter.kt create mode 100644 idea/src/org/jetbrains/kotlin/idea/debugger/evaluate/classLoading/ClassLoaderHandler.kt create mode 100644 idea/src/org/jetbrains/kotlin/idea/debugger/evaluate/classLoading/ClassLoadingAdapter.kt create mode 100644 idea/src/org/jetbrains/kotlin/idea/debugger/evaluate/classLoading/ClassToLoad.kt create mode 100644 idea/src/org/jetbrains/kotlin/idea/debugger/evaluate/classLoading/OrdinaryClassLoadingAdapter.kt diff --git a/idea/idea-android/idea-android.iml b/idea/idea-android/idea-android.iml index 2a5b3985a11..d7e839afa2d 100644 --- a/idea/idea-android/idea-android.iml +++ b/idea/idea-android/idea-android.iml @@ -24,5 +24,6 @@ + \ No newline at end of file diff --git a/idea/idea-android/src/org/jetbrains/kotlin/android/debugger/AndroidDexWrapper.kt b/idea/idea-android/src/org/jetbrains/kotlin/android/debugger/AndroidDexWrapper.kt new file mode 100644 index 00000000000..9c1bff78ce7 --- /dev/null +++ b/idea/idea-android/src/org/jetbrains/kotlin/android/debugger/AndroidDexWrapper.kt @@ -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): 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) + } +} \ No newline at end of file diff --git a/idea/idea-android/src/org/jetbrains/kotlin/android/debugger/AndroidDexerImpl.kt b/idea/idea-android/src/org/jetbrains/kotlin/android/debugger/AndroidDexerImpl.kt new file mode 100644 index 00000000000..0cafa358089 --- /dev/null +++ b/idea/idea-android/src/org/jetbrains/kotlin/android/debugger/AndroidDexerImpl.kt @@ -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): 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 + } +} \ No newline at end of file diff --git a/idea/src/META-INF/android.xml b/idea/src/META-INF/android.xml index 77ae4f2fa91..1b5e34fe10a 100644 --- a/idea/src/META-INF/android.xml +++ b/idea/src/META-INF/android.xml @@ -84,6 +84,8 @@ + + diff --git a/idea/src/META-INF/extensions/ide.xml b/idea/src/META-INF/extensions/ide.xml index 907aca56000..d83cc27d7ce 100644 --- a/idea/src/META-INF/extensions/ide.xml +++ b/idea/src/META-INF/extensions/ide.xml @@ -6,6 +6,10 @@ + + diff --git a/idea/src/org/jetbrains/kotlin/idea/debugger/evaluate/KotlinDebuggerCaches.kt b/idea/src/org/jetbrains/kotlin/idea/debugger/evaluate/KotlinDebuggerCaches.kt index d01efd09c47..f4fc6fac609 100644 --- a/idea/src/org/jetbrains/kotlin/idea/debugger/evaluate/KotlinDebuggerCaches.kt +++ b/idea/src/org/jetbrains/kotlin/idea/debugger/evaluate/KotlinDebuggerCaches.kt @@ -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>, + val classes: List, val sourcePosition: SourcePosition, val parameters: ParametersDescriptor ) diff --git a/idea/src/org/jetbrains/kotlin/idea/debugger/evaluate/KotlinEvaluationBuilder.kt b/idea/src/org/jetbrains/kotlin/idea/debugger/evaluate/KotlinEvaluationBuilder.kt index c1ea26f6290..bd96a5df65f 100644 --- a/idea/src/org/jetbrains/kotlin/idea/debugger/evaluate/KotlinEvaluationBuilder.kt +++ b/idea/src/org/jetbrains/kotlin/idea/debugger/evaluate/KotlinEvaluationBuilder.kt @@ -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?): 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 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 } diff --git a/idea/src/org/jetbrains/kotlin/idea/debugger/evaluate/classLoading/AbstractAndroidClassLoadingAdapter.kt b/idea/src/org/jetbrains/kotlin/idea/debugger/evaluate/classLoading/AbstractAndroidClassLoadingAdapter.kt new file mode 100644 index 00000000000..7d648a386ff --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/debugger/evaluate/classLoading/AbstractAndroidClassLoadingAdapter.kt @@ -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): 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 \ No newline at end of file diff --git a/idea/src/org/jetbrains/kotlin/idea/debugger/evaluate/classLoading/AndroidDexer.kt b/idea/src/org/jetbrains/kotlin/idea/debugger/evaluate/classLoading/AndroidDexer.kt new file mode 100644 index 00000000000..032726ac12a --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/debugger/evaluate/classLoading/AndroidDexer.kt @@ -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( + "org.jetbrains.kotlin.androidDexer", AndroidDexer::class.java) + + fun dex(classes: Collection): ByteArray? +} \ No newline at end of file diff --git a/idea/src/org/jetbrains/kotlin/idea/debugger/evaluate/classLoading/AndroidOClassLoadingAdapter.kt b/idea/src/org/jetbrains/kotlin/idea/debugger/evaluate/classLoading/AndroidOClassLoadingAdapter.kt new file mode 100644 index 00000000000..a06fae047d9 --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/debugger/evaluate/classLoading/AndroidOClassLoadingAdapter.kt @@ -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): 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): 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) + } +} \ No newline at end of file diff --git a/idea/src/org/jetbrains/kotlin/idea/debugger/evaluate/classLoading/ClassLoaderHandler.kt b/idea/src/org/jetbrains/kotlin/idea/debugger/evaluate/classLoading/ClassLoaderHandler.kt new file mode 100644 index 00000000000..94f4eb18de4 --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/debugger/evaluate/classLoading/ClassLoaderHandler.kt @@ -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() {} +} \ No newline at end of file diff --git a/idea/src/org/jetbrains/kotlin/idea/debugger/evaluate/classLoading/ClassLoadingAdapter.kt b/idea/src/org/jetbrains/kotlin/idea/debugger/evaluate/classLoading/ClassLoadingAdapter.kt new file mode 100644 index 00000000000..578d42eef75 --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/debugger/evaluate/classLoading/ClassLoadingAdapter.kt @@ -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): 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): Boolean + + fun loadClasses(context: EvaluationContextImpl, classes: Collection): ClassLoaderHandler +} \ No newline at end of file diff --git a/idea/src/org/jetbrains/kotlin/idea/debugger/evaluate/classLoading/ClassToLoad.kt b/idea/src/org/jetbrains/kotlin/idea/debugger/evaluate/classLoading/ClassToLoad.kt new file mode 100644 index 00000000000..571d242ffd3 --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/debugger/evaluate/classLoading/ClassToLoad.kt @@ -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) \ No newline at end of file diff --git a/idea/src/org/jetbrains/kotlin/idea/debugger/evaluate/classLoading/OrdinaryClassLoadingAdapter.kt b/idea/src/org/jetbrains/kotlin/idea/debugger/evaluate/classLoading/OrdinaryClassLoadingAdapter.kt new file mode 100644 index 00000000000..35aa320040b --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/debugger/evaluate/classLoading/OrdinaryClassLoadingAdapter.kt @@ -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): Boolean { + val classLoader = context.classLoader + return classLoader != null && !classLoader.isDalvikClassLoader + } + + override fun loadClasses(context: EvaluationContextImpl, classes: Collection): 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, + 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() + } + } + } +} \ No newline at end of file diff --git a/idea/src/org/jetbrains/kotlin/idea/debugger/evaluate/compilingEvaluator.kt b/idea/src/org/jetbrains/kotlin/idea/debugger/evaluate/compilingEvaluator.kt index 6ae3644140e..e0064beffe8 100644 --- a/idea/src/org/jetbrains/kotlin/idea/debugger/evaluate/compilingEvaluator.kt +++ b/idea/src/org/jetbrains/kotlin/idea/debugger/evaluate/compilingEvaluator.kt @@ -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>) { - val process = evaluationContext.debugProcess - - val classLoader: ClassLoaderReference +fun loadClassesSafely(evaluationContext: EvaluationContextImpl, classes: Collection): 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>, - 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): 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 } -} +} \ No newline at end of file diff --git a/idea/tests/org/jetbrains/kotlin/idea/debugger/evaluate/AbstractKotlinEvaluateExpressionTest.kt b/idea/tests/org/jetbrains/kotlin/idea/debugger/evaluate/AbstractKotlinEvaluateExpressionTest.kt index dba5591b0f3..18319ad4b78 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/debugger/evaluate/AbstractKotlinEvaluateExpressionTest.kt +++ b/idea/tests/org/jetbrains/kotlin/idea/debugger/evaluate/AbstractKotlinEvaluateExpressionTest.kt @@ -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) } }