diff --git a/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/KotlinStackFrame.kt b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/KotlinStackFrame.kt index 00172f19465..a4b81cd891e 100644 --- a/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/KotlinStackFrame.kt +++ b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/KotlinStackFrame.kt @@ -16,6 +16,7 @@ package org.jetbrains.kotlin.idea.debugger +import com.intellij.debugger.engine.DebugProcessImpl import com.intellij.debugger.DebuggerContext import com.intellij.debugger.engine.JavaStackFrame import com.intellij.debugger.engine.JavaValue @@ -50,6 +51,7 @@ import org.jetbrains.kotlin.codegen.inline.INLINE_FUN_VAR_SUFFIX import org.jetbrains.kotlin.codegen.inline.isFakeLocalVariableForInline import org.jetbrains.org.objectweb.asm.Type as AsmType import org.jetbrains.kotlin.utils.getSafe +import org.jetbrains.kotlin.idea.debugger.evaluate.KotlinDebuggerEvaluator import java.lang.reflect.Modifier import java.util.* @@ -60,6 +62,13 @@ class KotlinStackFrame(frame: StackFrameProxyImpl) : JavaStackFrame(StackFrameDe private val kotlinVariableViewService = ToggleKotlinVariablesState.getService() + private val kotlinEvaluator by lazy { + val debugProcess = descriptor.debugProcess as DebugProcessImpl // Cast as in JavaStackFrame + KotlinDebuggerEvaluator(debugProcess, this@KotlinStackFrame) + } + + override fun getEvaluator() = kotlinEvaluator + override fun superBuildVariables(evaluationContext: EvaluationContextImpl, children: XValueChildrenList) { if (!kotlinVariableViewService.kotlinVariableView) { return super.superBuildVariables(evaluationContext, children) diff --git a/idea/jvm-debugger/jvm-debugger-evaluation/src/org/jetbrains/kotlin/idea/debugger/evaluate/KotlinCodeFragmentFactory.kt b/idea/jvm-debugger/jvm-debugger-evaluation/src/org/jetbrains/kotlin/idea/debugger/evaluate/KotlinCodeFragmentFactory.kt index 5ce72737d84..771a42a5b16 100644 --- a/idea/jvm-debugger/jvm-debugger-evaluation/src/org/jetbrains/kotlin/idea/debugger/evaluate/KotlinCodeFragmentFactory.kt +++ b/idea/jvm-debugger/jvm-debugger-evaluation/src/org/jetbrains/kotlin/idea/debugger/evaluate/KotlinCodeFragmentFactory.kt @@ -68,7 +68,7 @@ class KotlinCodeFragmentFactory : CodeFragmentFactory() { } val codeFragment = constructor(project, "fragment.kt", item.text, initImports(item.imports), contextElement) - supplyDebugLabels(codeFragment, context) + supplyDebugInformation(item, codeFragment, context) codeFragment.putCopyableUserData(KtCodeFragment.RUNTIME_TYPE_EVALUATOR, { expression: KtExpression -> val debuggerContext = DebuggerManagerEx.getInstanceEx(project).context @@ -139,10 +139,16 @@ class KotlinCodeFragmentFactory : CodeFragmentFactory() { return codeFragment } - private fun supplyDebugLabels(codeFragment: KtCodeFragment, context: PsiElement?) { + private fun supplyDebugInformation(item: TextWithImports, codeFragment: KtCodeFragment, context: PsiElement?) { val project = codeFragment.project val debugProcess = getDebugProcess(project, context) ?: return + DebugLabelPropertyDescriptorProvider(codeFragment, debugProcess).supplyDebugLabels() + + val evaluator = debugProcess.session.xDebugSession?.currentStackFrame?.evaluator + if (evaluator is KotlinDebuggerEvaluator) { + codeFragment.putUserData(EVALUATION_TYPE, evaluator.getType(item)) + } } private fun getDebugProcess(project: Project, context: PsiElement?): DebugProcessImpl? { @@ -295,6 +301,8 @@ class KotlinCodeFragmentFactory : CodeFragmentFactory() { @get:TestOnly val DEBUG_CONTEXT_FOR_TESTS: Key = Key.create("DEBUG_CONTEXT_FOR_TESTS") + val EVALUATION_TYPE: Key = Key.create("DEBUG_EVALUATION_TYPE") + const val FAKE_JAVA_CONTEXT_FUNCTION_NAME = "_java_locals_debug_fun_" const val FAKE_JAVA_THIS_NAME = "\$this\$_java_locals_debug_fun_" diff --git a/idea/jvm-debugger/jvm-debugger-evaluation/src/org/jetbrains/kotlin/idea/debugger/evaluate/KotlinEvaluatorBuilder.kt b/idea/jvm-debugger/jvm-debugger-evaluation/src/org/jetbrains/kotlin/idea/debugger/evaluate/KotlinEvaluatorBuilder.kt index 94a707469fa..e501109b27b 100644 --- a/idea/jvm-debugger/jvm-debugger-evaluation/src/org/jetbrains/kotlin/idea/debugger/evaluate/KotlinEvaluatorBuilder.kt +++ b/idea/jvm-debugger/jvm-debugger-evaluation/src/org/jetbrains/kotlin/idea/debugger/evaluate/KotlinEvaluatorBuilder.kt @@ -22,6 +22,7 @@ import com.intellij.debugger.engine.evaluation.EvaluateException import com.intellij.debugger.engine.evaluation.EvaluateExceptionUtil import com.intellij.debugger.engine.evaluation.EvaluationContextImpl import com.intellij.debugger.engine.evaluation.expression.* +import com.intellij.lang.java.JavaLanguage import com.intellij.openapi.diagnostic.Attachment import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.progress.ProcessCanceledException @@ -42,6 +43,7 @@ import org.jetbrains.kotlin.diagnostics.DiagnosticFactory import org.jetbrains.kotlin.diagnostics.Errors import org.jetbrains.kotlin.diagnostics.Severity import org.jetbrains.kotlin.diagnostics.rendering.DefaultErrorMessages +import org.jetbrains.kotlin.idea.KotlinLanguage import org.jetbrains.kotlin.idea.core.util.attachmentByPsiFile import org.jetbrains.kotlin.idea.core.util.mergeAttachments import org.jetbrains.kotlin.idea.core.util.runInReadActionWithWriteActionPriorityWithPCE @@ -65,6 +67,8 @@ import org.jetbrains.kotlin.resolve.isInlineClassType import org.jetbrains.kotlin.resolve.jvm.AsmTypes import org.jetbrains.org.objectweb.asm.* import org.jetbrains.org.objectweb.asm.tree.ClassNode +import org.jetbrains.kotlin.idea.debugger.evaluate.EvaluationStatus.EvaluationContextLanguage +import org.jetbrains.kotlin.idea.debugger.evaluate.compilingEvaluator.ClassLoadingResult import java.util.* internal val LOG = Logger.getInstance(KotlinEvaluator::class.java) @@ -93,35 +97,68 @@ class KotlinEvaluator(val codeFragment: KtCodeFragment, val sourcePosition: Sour return context.debugProcess.virtualMachineProxy.mirrorOfVoid() } + val status = EvaluationStatus() + + val evaluationType = codeFragment.getUserData(KotlinCodeFragmentFactory.EVALUATION_TYPE) + if (evaluationType != null) { + status.evaluationType(evaluationType) + } + + val language = when (codeFragment.language) { + KotlinLanguage.INSTANCE -> EvaluationContextLanguage.Kotlin + JavaLanguage.INSTANCE -> EvaluationContextLanguage.Java + else -> EvaluationContextLanguage.Other + } + + status.contextLanguage(language) + + try { + return evaluateWithStatus(context, status) + } finally { + status.send() + } + } + + private fun evaluateWithStatus(context: EvaluationContextImpl, status: EvaluationStatus): Any? { if (DumbService.getInstance(codeFragment.project).isDumb) { evaluationException("Code fragment evaluation is not available in the dumb mode") } - val frameProxy = context.frameProxy - ?: evaluationException("Cannot evaluate a code fragment: frame proxy is not available") + val frameProxy = context.frameProxy ?: run { + status.error(EvaluationError.NoFrameProxy) + evaluationException("Cannot evaluate a code fragment: frame proxy is not available") + } - val operatingThread = context.suspendContext.thread - ?: evaluationException("Cannot evaluate a code fragment: thread is not available") + val operatingThread = context.suspendContext.thread ?: run { + status.error(EvaluationError.ThreadNotAvailable) + evaluationException("Cannot evaluate a code fragment: thread is not available") + } if (!operatingThread.isSuspended) { + status.error(EvaluationError.ThreadNotSuspended) evaluationException("Evaluation is available only for the suspended threads") } try { val executionContext = ExecutionContext(context, frameProxy) - return evaluateSafe(executionContext) + return evaluateSafe(executionContext, status) } catch (e: EvaluateException) { + status.error(EvaluationError.EvaluateException) throw e } catch (e: ProcessCanceledException) { + status.error(EvaluationError.ProcessCancelledException) evaluationException(e) } catch (e: Eval4JInterpretingException) { + status.error(EvaluationError.InterpretingException) evaluationException(e.cause) } catch (e: Exception) { val isSpecialException = isSpecialException(e) if (isSpecialException) { + status.error(EvaluationError.SpecialException) evaluationException(e) } + status.error(EvaluationError.GenericException) reportError(codeFragment, sourcePosition, e.message ?: "An exception occurred", e) val cause = if (e.message != null) ": ${e.message}" else "" @@ -129,38 +166,44 @@ class KotlinEvaluator(val codeFragment: KtCodeFragment, val sourcePosition: Sour } } - private fun evaluateSafe(context: ExecutionContext): Any? { - fun compilerFactory(): CompiledDataDescriptor = compileCodeFragment(context) + private fun evaluateSafe(context: ExecutionContext, status: EvaluationStatus): Any? { + fun compilerFactory(): CompiledDataDescriptor = compileCodeFragment(context, status) - val (compiledData, isCompiledDataFromCache) = compileCodeFragmentCacheAware(codeFragment, sourcePosition, ::compilerFactory) - val classLoaderRef = loadClassesSafely(context, compiledData.classes) + val (compiledData, _) = compileCodeFragmentCacheAware(codeFragment, sourcePosition, ::compilerFactory, force = false) + + val classLoadingResult = loadClassesSafely(context, compiledData.classes) + val classLoaderRef = (classLoadingResult as? ClassLoadingResult.Success)?.classLoader + + if (classLoadingResult is ClassLoadingResult.Failure) { + status.classLoadingFailed() + } val result = if (classLoaderRef != null) { - evaluateWithCompilation(context, compiledData, classLoaderRef) - ?: evaluateWithEval4J(context, compiledData, classLoaderRef) + try { + status.usedEvaluator(EvaluationStatus.EvaluatorType.Bytecode) + return evaluateWithCompilation(context, compiledData, classLoaderRef) + } catch (e: Throwable) { + status.compilingEvaluatorFailed() + LOG.warn("Compiling evaluator failed", e) + + status.usedEvaluator(EvaluationStatus.EvaluatorType.Eval4j) + evaluateWithEval4J(context, compiledData, classLoaderRef) + } } else { + status.usedEvaluator(EvaluationStatus.EvaluatorType.Eval4j) evaluateWithEval4J(context, compiledData, classLoaderRef) } - // 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) { - val (recompiledData, _) = compileCodeFragmentCacheAware(codeFragment, sourcePosition, ::compilerFactory, force = true) - return evaluateWithEval4J(context, recompiledData, classLoaderRef).toJdiValue(context) - } - - return when (result) { - is InterpreterResult -> result.toJdiValue(context) - else -> result - } + return result.toJdiValue(context) } - private fun compileCodeFragment(context: ExecutionContext): CompiledDataDescriptor { + private fun compileCodeFragment(context: ExecutionContext, status: EvaluationStatus): CompiledDataDescriptor { val debugProcess = context.debugProcess - var analysisResult = checkForErrors(codeFragment, debugProcess) + var analysisResult = analyze(codeFragment, status, debugProcess) if (codeFragment.wrapToStringIfNeeded(analysisResult.bindingContext)) { // Repeat analysis with toString() added - analysisResult = checkForErrors(codeFragment, debugProcess) + analysisResult = analyze(codeFragment, status, debugProcess) } val (bindingContext) = runReadAction { @@ -172,8 +215,13 @@ class KotlinEvaluator(val codeFragment: KtCodeFragment, val sourcePosition: Sour val moduleDescriptor = analysisResult.moduleDescriptor - val result = CodeFragmentCompiler(context).compile(codeFragment, bindingContext, moduleDescriptor) - return createCompiledDataDescriptor(result, sourcePosition) + try { + val result = CodeFragmentCompiler(context).compile(codeFragment, bindingContext, moduleDescriptor) + return createCompiledDataDescriptor(result, sourcePosition) + } catch (e: Throwable) { + status.error(EvaluationError.BackendException) + throw e + } } private fun KtCodeFragment.wrapToStringIfNeeded(bindingContext: BindingContext): Boolean { @@ -205,7 +253,7 @@ class KotlinEvaluator(val codeFragment: KtCodeFragment, val sourcePosition: Sour val files: List ) - private fun checkForErrors(codeFragment: KtCodeFragment, debugProcess: DebugProcessImpl): ErrorCheckingResult { + private fun analyze(codeFragment: KtCodeFragment, status: EvaluationStatus, debugProcess: DebugProcessImpl): ErrorCheckingResult { return runInReadActionWithWriteActionPriorityWithPCE { try { AnalyzingUtils.checkForSyntacticErrors(codeFragment) @@ -221,6 +269,7 @@ class KotlinEvaluator(val codeFragment: KtCodeFragment, val sourcePosition: Sour val analysisResult = resolutionFacade.analyzeWithAllCompilerChecks(filesToAnalyze) if (analysisResult.isError()) { + status.error(EvaluationError.FrontendException) evaluationException(analysisResult.error) } @@ -229,7 +278,10 @@ class KotlinEvaluator(val codeFragment: KtCodeFragment, val sourcePosition: Sour bindingContext.diagnostics .filter { it.factory !in IGNORED_DIAGNOSTICS } .firstOrNull { it.severity == Severity.ERROR && it.psiElement.containingFile == codeFragment } - ?.let { evaluationException(DefaultErrorMessages.render(it)) } + ?.let { + status.error(EvaluationError.ErrorsInCode) + evaluationException(DefaultErrorMessages.render(it)) + } ErrorCheckingResult(bindingContext, analysisResult.moduleDescriptor, Collections.singletonList(codeFragment)) } @@ -240,17 +292,12 @@ class KotlinEvaluator(val codeFragment: KtCodeFragment, val sourcePosition: Sour compiledData: CompiledDataDescriptor, classLoader: ClassLoaderReference ): Value? { - return try { - runEvaluation(context, compiledData, classLoader) { args -> - val mainClassType = context.findClass(GENERATED_CLASS_NAME, classLoader) as? ClassType - ?: error("Can not find class \"$GENERATED_CLASS_NAME\"") - val mainMethod = mainClassType.methods().single { it.name() == GENERATED_FUNCTION_NAME } - val returnValue = context.invokeMethod(mainClassType, mainMethod, args) - EvaluatorValueConverter(context).unref(returnValue) - } - } catch (e: Throwable) { - LOG.error("Unable to evaluate the expression with compilation", e) - return null + return runEvaluation(context, compiledData, classLoader) { args -> + val mainClassType = context.findClass(GENERATED_CLASS_NAME, classLoader) as? ClassType + ?: error("Can not find class \"$GENERATED_CLASS_NAME\"") + val mainMethod = mainClassType.methods().single { it.name() == GENERATED_FUNCTION_NAME } + val returnValue = context.invokeMethod(mainClassType, mainMethod, args) + EvaluatorValueConverter(context).unref(returnValue) } } diff --git a/idea/jvm-debugger/jvm-debugger-evaluation/src/org/jetbrains/kotlin/idea/debugger/evaluate/compilingEvaluator.kt b/idea/jvm-debugger/jvm-debugger-evaluation/src/org/jetbrains/kotlin/idea/debugger/evaluate/compilingEvaluator.kt index 59148d2d32c..1c6a6a3f130 100644 --- a/idea/jvm-debugger/jvm-debugger-evaluation/src/org/jetbrains/kotlin/idea/debugger/evaluate/compilingEvaluator.kt +++ b/idea/jvm-debugger/jvm-debugger-evaluation/src/org/jetbrains/kotlin/idea/debugger/evaluate/compilingEvaluator.kt @@ -23,21 +23,32 @@ import org.jetbrains.kotlin.idea.debugger.evaluate.LOG import org.jetbrains.kotlin.idea.debugger.evaluate.classLoading.ClassLoadingAdapter import org.jetbrains.kotlin.idea.debugger.evaluate.classLoading.ClassToLoad -fun loadClassesSafely(context: ExecutionContext, classes: Collection): ClassLoaderReference? { +sealed class ClassLoadingResult { + class Success(val classLoader: ClassLoaderReference) : ClassLoadingResult() + class Failure(val error: Throwable) : ClassLoadingResult() + object NotNeeded : ClassLoadingResult() +} + +fun loadClassesSafely(context: ExecutionContext, classes: Collection): ClassLoadingResult { + if (classes.isEmpty()) { + return ClassLoadingResult.NotNeeded + } + return try { - loadClasses(context, classes) + val cl = loadClasses(context, classes) + if (cl != null) { + ClassLoadingResult.Success(cl) + } else { + ClassLoadingResult.NotNeeded + } } catch (e: EvaluateException) { throw e } catch (e: Throwable) { LOG.debug("Failed to evaluate expression", e) - null + ClassLoadingResult.Failure(e) } } fun loadClasses(context: ExecutionContext, classes: Collection): ClassLoaderReference? { - if (classes.isEmpty()) { - return null - } - return ClassLoadingAdapter.loadClasses(context, classes) } \ No newline at end of file diff --git a/idea/jvm-debugger/jvm-debugger-evaluation/src/org/jetbrains/kotlin/idea/debugger/evaluate/evaluate/EvaluationStatus.kt b/idea/jvm-debugger/jvm-debugger-evaluation/src/org/jetbrains/kotlin/idea/debugger/evaluate/evaluate/EvaluationStatus.kt new file mode 100644 index 00000000000..2108a069a55 --- /dev/null +++ b/idea/jvm-debugger/jvm-debugger-evaluation/src/org/jetbrains/kotlin/idea/debugger/evaluate/evaluate/EvaluationStatus.kt @@ -0,0 +1,83 @@ +/* + * Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.idea.debugger.evaluate + +import org.jetbrains.kotlin.idea.statistics.FUSEventGroups +import org.jetbrains.kotlin.idea.statistics.KotlinFUSLogger + +class EvaluationStatus { + private var error: EvaluationError? = null + private var values = mutableMapOf() + private var evaluationTypeSet: Boolean = false + + fun error(kind: EvaluationError) { + if (error == null) { + error = kind + } + } + + fun flag(name: String, value: Boolean) { + values[name] = value.toString() + } + + fun > value(name: String, value: T) { + values[name] = value.name + } + + fun evaluationType(type: KotlinDebuggerEvaluator.EvaluationType) { + evaluationTypeSet = true + value("evaluationType", type) + } + + fun send() { + if (!evaluationTypeSet) { + return + } + + val statusName = error?.name ?: "Success" + KotlinFUSLogger.log(FUSEventGroups.DebugEval, statusName, values) + } + + enum class EvaluatorType { + Bytecode, Eval4j + } + + enum class EvaluationContextLanguage { + Java, Kotlin, Other + } +} + +enum class EvaluationError { + NoFrameProxy, + ThreadNotAvailable, + ThreadNotSuspended, + + ProcessCancelledException, + InterpretingException, + EvaluateException, + SpecialException, + GenericException, + + FrontendException, + BackendException, + ErrorsInCode +} + +fun EvaluationStatus.classLoadingFailed() { + flag("classLoadingFailed", true) +} + +fun EvaluationStatus.compilingEvaluatorFailed() { + flag("compilingEvaluatorFailed", true) +} + +fun EvaluationStatus.usedEvaluator(evaluator: EvaluationStatus.EvaluatorType) { + value("evaluator", evaluator) +} + +fun EvaluationStatus.contextLanguage(language: EvaluationStatus.EvaluationContextLanguage) { + value("contextLanguage", language) +} \ No newline at end of file diff --git a/idea/jvm-debugger/jvm-debugger-evaluation/src/org/jetbrains/kotlin/idea/debugger/evaluate/evaluate/KotlinDebuggerEvaluator.kt b/idea/jvm-debugger/jvm-debugger-evaluation/src/org/jetbrains/kotlin/idea/debugger/evaluate/evaluate/KotlinDebuggerEvaluator.kt new file mode 100644 index 00000000000..4fe79f808b3 --- /dev/null +++ b/idea/jvm-debugger/jvm-debugger-evaluation/src/org/jetbrains/kotlin/idea/debugger/evaluate/evaluate/KotlinDebuggerEvaluator.kt @@ -0,0 +1,76 @@ +/* + * Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.idea.debugger.evaluate + +import com.intellij.debugger.engine.DebugProcessImpl +import com.intellij.debugger.engine.JavaDebuggerEvaluator +import com.intellij.debugger.engine.JavaStackFrame +import com.intellij.debugger.engine.evaluation.TextWithImports +import com.intellij.openapi.util.text.StringUtil +import com.intellij.xdebugger.XExpression +import com.intellij.xdebugger.XSourcePosition +import com.intellij.xdebugger.frame.XValue +import com.intellij.xdebugger.impl.evaluate.quick.XValueHint +import com.intellij.xdebugger.impl.ui.tree.nodes.EvaluatingExpressionRootNode +import com.intellij.xdebugger.impl.ui.tree.nodes.WatchNodeImpl +import java.util.* + +private data class Key(val text: String, val imports: String) { + constructor(expr: XExpression) : this(expr.expression, StringUtil.notNullize(expr.customInfo)) + constructor(text: TextWithImports) : this(text.text, text.imports) +} + +class KotlinDebuggerEvaluator( + debugProcess: DebugProcessImpl?, + stackFrame: JavaStackFrame? +) : JavaDebuggerEvaluator(debugProcess, stackFrame) { + private val types = HashMap() + + fun getType(text: TextWithImports): EvaluationType { + return types[Key(text)] ?: EvaluationType.UNKNOWN + } + + override fun evaluate(expression: XExpression, callback: XEvaluationCallback, expressionPosition: XSourcePosition?) { + val key = Key(expression) + val type = getType(callback) + if (type != null) { + types[key] = type + } + + val wrappedCallback = object : XEvaluationCallback { + override fun errorOccurred(errorMessage: String) { + types.remove(key) + callback.errorOccurred(errorMessage) + } + + override fun evaluated(result: XValue) { + types.remove(key) + callback.evaluated(result) + } + } + + super.evaluate(expression, wrappedCallback, expressionPosition) + } + + private fun getType(callback: XEvaluationCallback): EvaluationType? { + val name = callback.javaClass.name + for (value in EvaluationType.values()) { + val clazz = value.clazz ?: continue + if (name.startsWith(clazz.name)) { + return value + } + } + + return null + } + + enum class EvaluationType(val clazz: Class<*>?) { + WATCH(WatchNodeImpl::class.java), + WINDOW(EvaluatingExpressionRootNode::class.java), + POPUP(XValueHint::class.java), + UNKNOWN(null); + } +} \ No newline at end of file