diff --git a/idea/src/org/jetbrains/kotlin/idea/debugger/NoStrataPositionManagerHelper.kt b/idea/src/org/jetbrains/kotlin/idea/debugger/NoStrataPositionManagerHelper.kt index 1c61612f4ee..ca37c7eb122 100644 --- a/idea/src/org/jetbrains/kotlin/idea/debugger/NoStrataPositionManagerHelper.kt +++ b/idea/src/org/jetbrains/kotlin/idea/debugger/NoStrataPositionManagerHelper.kt @@ -38,10 +38,6 @@ import org.jetbrains.kotlin.utils.getOrPutNullable import org.jetbrains.org.objectweb.asm.* import java.util.* -// TODO: Don't read same bytecode file again and again -// TODO: Build line mapping for the whole file -// TODO: Quick caching for the same location - fun noStrataLineNumber(location: Location, isDexDebug: Boolean, project: Project, preferInlined: Boolean = false): Int { if (isDexDebug) { if (!preferInlined) { @@ -68,33 +64,40 @@ fun getLastLineNumberForLocation(location: Location, project: Project, searchSco val fileName = location.sourceName() val method = location.method() ?: return null + val name = method.name() ?: return null + val signature = method.signature() ?: return null - val bytes = findAndReadClassFile(fqName, fileName, project, searchScope, { isInlineFunctionLineNumber(it, lineNumber, project) }) ?: return null + val debugInfo = findAndReadClassFile(fqName, fileName, project, searchScope, { isInlineFunctionLineNumber(it, lineNumber, project) }) ?: return null - fun readLineNumberTableMapping(bytes: ByteArray): Map> { - val labelsToAllStrings = HashMap>() + val lineMapping = debugInfo.lineTableMapping[BytecodeMethodKey(name, signature)] ?: return null + return lineMapping.values.firstOrNull { it.contains(lineNumber) }?.last() +} - ClassReader(bytes).accept(object : ClassVisitor(InlineCodegenUtil.API) { - override fun visitMethod(access: Int, name: String?, desc: String?, signature: String?, exceptions: Array?): MethodVisitor? { - if (!(name == method.name() && desc == method.signature())) { - return null - } +fun readLineNumberTableMapping(bytes: ByteArray): Map>> { + val lineNumberMapping = HashMap>>() - return object : MethodVisitor(Opcodes.ASM5, null) { - override fun visitLineNumber(line: Int, start: Label?) { - if (start != null) { - labelsToAllStrings.getOrPutNullable(start.toString(), { LinkedHashSet() }).add(line) - } + ClassReader(bytes).accept(object : ClassVisitor(InlineCodegenUtil.API) { + override fun visitMethod(access: Int, name: String?, desc: String?, signature: String?, exceptions: Array?): MethodVisitor? { + if (name == null || desc == null) { + // TODO: check constructors + return null + } + + val methodKey = BytecodeMethodKey(name, desc) + val methodLinesMapping = HashMap>() + lineNumberMapping[methodKey] = methodLinesMapping + + return object : MethodVisitor(Opcodes.ASM5, null) { + override fun visitLineNumber(line: Int, start: Label?) { + if (start != null) { + methodLinesMapping.getOrPutNullable(start.toString(), { LinkedHashSet() }).add(line) } } } - }, ClassReader.SKIP_FRAMES and ClassReader.SKIP_CODE) + } + }, ClassReader.SKIP_FRAMES and ClassReader.SKIP_CODE) - return labelsToAllStrings - } - - val lineMapping = readLineNumberTableMapping(bytes) - return lineMapping.values.firstOrNull { it.contains(lineNumber) }?.last() + return lineNumberMapping } internal fun getOriginalPositionOfInlinedLine(location: Location, project: Project): Pair? { @@ -103,14 +106,16 @@ internal fun getOriginalPositionOfInlinedLine(location: Location, project: Proje val fileName = location.sourceName() val searchScope = GlobalSearchScope.allScope(project) - val bytes = findAndReadClassFile(fqName, fileName, project, searchScope, { isInlineFunctionLineNumber(it, lineNumber, project) }) ?: return null - val smapData = readDebugInfo(bytes) ?: return null + val debugInfo = findAndReadClassFile(fqName, fileName, project, searchScope, { isInlineFunctionLineNumber(it, lineNumber, project) }) ?: + return null + val smapData = debugInfo.smapData ?: return null + return mapStacktraceLineToSource(smapData, lineNumber, project, SourceLineKind.EXECUTED_LINE, searchScope) } internal fun findAndReadClassFile( fqName: FqName, fileName: String, project: Project, searchScope: GlobalSearchScope, - fileFilter: (VirtualFile) -> Boolean): ByteArray? { + fileFilter: (VirtualFile) -> Boolean): BytecodeDebugInfo? { val internalName = fqName.asString().replace('.', '/') val jvmClassName = JvmClassName.byInternalName(internalName) @@ -151,8 +156,8 @@ private fun inlinedLinesNumbers( val virtualFile = file.virtualFile ?: return listOf() - val bytes = readClassFile(project, jvmClassName, virtualFile) ?: return listOf() - val smapData = readDebugInfo(bytes) ?: return listOf() + val debugInfo = readClassFile(project, jvmClassName, virtualFile) ?: return listOf() + val smapData = debugInfo.smapData ?: return listOf() val smap = smapData.kotlinStrata ?: return listOf() @@ -171,5 +176,5 @@ private fun inlinedLinesNumbers( @Volatile var emulateDexDebugInTests: Boolean = false fun DebugProcess.isDexDebug() = - (emulateDexDebugInTests && ApplicationManager.getApplication ().isUnitTestMode) || + (emulateDexDebugInTests && ApplicationManager.getApplication().isUnitTestMode) || (this.virtualMachineProxy as? VirtualMachineProxyImpl)?.virtualMachine?.name() == "Dalvik" // TODO: check other machine names \ No newline at end of file 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 37a8381a0e1..07de78d4a9f 100644 --- a/idea/src/org/jetbrains/kotlin/idea/debugger/evaluate/KotlinDebuggerCaches.kt +++ b/idea/src/org/jetbrains/kotlin/idea/debugger/evaluate/KotlinDebuggerCaches.kt @@ -21,6 +21,7 @@ import com.intellij.debugger.engine.evaluation.EvaluationContextImpl import com.intellij.openapi.components.ServiceManager import com.intellij.openapi.project.Project import com.intellij.openapi.roots.libraries.LibraryUtil +import com.intellij.openapi.vfs.VirtualFile import com.intellij.psi.PsiElement import com.intellij.psi.util.CachedValueProvider import com.intellij.psi.util.CachedValuesManager @@ -38,11 +39,15 @@ import org.jetbrains.kotlin.config.CompilerConfiguration import org.jetbrains.kotlin.descriptors.ClassDescriptor import org.jetbrains.kotlin.idea.caches.resolve.analyzeAndGetResult 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.WeakConcurrentBinaryStorage import org.jetbrains.kotlin.idea.util.application.runReadAction import org.jetbrains.kotlin.psi.KtCodeFragment import org.jetbrains.kotlin.psi.KtElement import org.jetbrains.kotlin.psi.KtFile import org.jetbrains.kotlin.resolve.DescriptorUtils +import org.jetbrains.kotlin.resolve.jvm.JvmClassName import org.jetbrains.kotlin.types.KotlinType import java.util.* import java.util.concurrent.ConcurrentHashMap @@ -69,6 +74,13 @@ class KotlinDebuggerCaches(project: Project) { PsiModificationTracker.MODIFICATION_COUNT) }, false) + private val binaryCache = CachedValuesManager.getManager(project).createCachedValue( + { + CachedValueProvider.Result( + WeakConcurrentBinaryStorage(), + PsiModificationTracker.MODIFICATION_COUNT) + }, false) + companion object { private val LOG = Logger.getLogger(KotlinDebuggerCaches::class.java)!! @@ -154,6 +166,14 @@ class KotlinDebuggerCaches(project: Project) { } } + fun readFileContent( + project: Project, + jvmName: JvmClassName, + file: VirtualFile): BytecodeDebugInfo? { + val cache = getInstance(project) + return cache.binaryCache.value[BinaryCacheKey(project, jvmName, file)] + } + private fun getElementToCreateTypeMapperForLibraryFile(element: PsiElement?) = runReadAction { element as? KtElement ?: PsiTreeUtil.getParentOfType(element, KtElement::class.java)!! } diff --git a/idea/src/org/jetbrains/kotlin/idea/debugger/smapUtil.kt b/idea/src/org/jetbrains/kotlin/idea/debugger/smapUtil.kt index 9a080aa99a5..e7132fbb88f 100644 --- a/idea/src/org/jetbrains/kotlin/idea/debugger/smapUtil.kt +++ b/idea/src/org/jetbrains/kotlin/idea/debugger/smapUtil.kt @@ -22,10 +22,13 @@ import com.intellij.openapi.project.Project import com.intellij.openapi.roots.ProjectFileIndex import com.intellij.openapi.vfs.VirtualFile import com.intellij.psi.search.GlobalSearchScope +import com.intellij.util.containers.ConcurrentWeakFactoryMap +import com.intellij.util.containers.ContainerUtil import org.jetbrains.kotlin.codegen.inline.FileMapping import org.jetbrains.kotlin.codegen.inline.InlineCodegenUtil import org.jetbrains.kotlin.codegen.inline.SMAP import org.jetbrains.kotlin.codegen.inline.SMAPParser +import org.jetbrains.kotlin.idea.debugger.evaluate.KotlinDebuggerCaches import org.jetbrains.kotlin.idea.refactoring.getLineCount import org.jetbrains.kotlin.idea.refactoring.toPsiFile import org.jetbrains.kotlin.idea.util.ProjectRootsUtil @@ -49,12 +52,38 @@ fun isInlineFunctionLineNumber(file: VirtualFile, lineNumber: Int, project: Proj return true } +class WeakConcurrentBinaryStorage : ConcurrentWeakFactoryMap() { + override fun create(key: BinaryCacheKey): BytecodeDebugInfo? { + val bytes = readClassFileImpl(key.project, key.jvmName, key.file) ?: return null + + val smapData = readDebugInfo(bytes) + val lineNumberMapping = readLineNumberTableMapping(bytes) + + return BytecodeDebugInfo(smapData, lineNumberMapping) + } + + override fun createMap(): Map { + return ContainerUtil.createConcurrentWeakKeyWeakValueMap() + } +} + fun readClassFile(project: Project, jvmName: JvmClassName, - file: VirtualFile): ByteArray? { + file: VirtualFile): BytecodeDebugInfo? { + return KotlinDebuggerCaches.readFileContent(project, jvmName, file) +} + +class BytecodeDebugInfo(val smapData: SmapData?, val lineTableMapping: Map>>) + +data class BytecodeMethodKey(val methodName: String, val signature: String) +data class BinaryCacheKey(val project: Project, val jvmName: JvmClassName, val file: VirtualFile) + +private fun readClassFileImpl(project: Project, + jvmName: JvmClassName, + file: VirtualFile): ByteArray? { val fqNameWithInners = jvmName.fqNameForClassNameWithoutDollars.tail(jvmName.packageFqName) - fun readFromLibrary() : ByteArray? { + fun readFromLibrary(): ByteArray? { if (!ProjectRootsUtil.isLibrarySourceFile(project, file)) return null val classId = ClassId(jvmName.packageFqName, Name.identifier(fqNameWithInners.asString())) @@ -84,12 +113,13 @@ fun readClassFile(project: Project, classByDirectory = findClassFileByPath(jvmName.packageFqName.asString(), className, androidTestOutputDir) ?: return null } + println("Read file: " + classByDirectory) return classByDirectory.readBytes() } - fun readFromSourceOutput() : ByteArray? = readFromOutput(false) + fun readFromSourceOutput(): ByteArray? = readFromOutput(false) - fun readFromTestOutput() : ByteArray? = readFromOutput(true) + fun readFromTestOutput(): ByteArray? = readFromOutput(true) return readFromLibrary() ?: readFromSourceOutput() ?: @@ -128,9 +158,9 @@ fun mapStacktraceLineToSource(smapData: SmapData, lineKind: SourceLineKind, searchScope: GlobalSearchScope): Pair? { val smap = when (lineKind) { - SourceLineKind.CALL_LINE -> smapData.kotlinDebugStrata - SourceLineKind.EXECUTED_LINE -> smapData.kotlinStrata - } ?: return null + SourceLineKind.CALL_LINE -> smapData.kotlinDebugStrata + SourceLineKind.EXECUTED_LINE -> smapData.kotlinStrata + } ?: return null val mappingInfo = smap.fileMappings.firstOrNull { it.getIntervalIfContains(line) != null @@ -166,7 +196,7 @@ class SmapData(debugInfo: String) { init { val intervals = debugInfo.split(SMAP.END).filter(String::isNotBlank) - when(intervals.count()) { + when (intervals.count()) { 1 -> { kotlinStrata = SMAPParser.parse(intervals[0] + SMAP.END) kotlinDebugStrata = null diff --git a/idea/src/org/jetbrains/kotlin/idea/filters/KotlinExceptionFilter.kt b/idea/src/org/jetbrains/kotlin/idea/filters/KotlinExceptionFilter.kt index b7344132f75..f5e60e94c32 100644 --- a/idea/src/org/jetbrains/kotlin/idea/filters/KotlinExceptionFilter.kt +++ b/idea/src/org/jetbrains/kotlin/idea/filters/KotlinExceptionFilter.kt @@ -75,8 +75,8 @@ class KotlinExceptionFilter(private val searchScope: GlobalSearchScope) : Filter private fun createHyperlinks(jvmName: JvmClassName, file: VirtualFile, line: Int, project: Project): InlineFunctionHyperLinkInfo? { if (!isInlineFunctionLineNumber(file, line, project)) return null - val bytes = readClassFile(project, jvmName, file) ?: return null - val smapData = readDebugInfo(bytes) ?: return null + val debugInfo = readClassFile(project, jvmName, file) ?: return null + val smapData = debugInfo.smapData ?: return null val inlineInfos = arrayListOf()