Cache debug information from bytecode in Android debug

This commit is contained in:
Nikolay Krasko
2016-10-28 22:29:07 +03:00
parent 78c76a2ca4
commit 1c955a64f3
4 changed files with 94 additions and 39 deletions
@@ -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<String, Set<Int>> {
val labelsToAllStrings = HashMap<String, MutableSet<Int>>()
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<out String>?): MethodVisitor? {
if (!(name == method.name() && desc == method.signature())) {
return null
}
fun readLineNumberTableMapping(bytes: ByteArray): Map<BytecodeMethodKey, Map<String, Set<Int>>> {
val lineNumberMapping = HashMap<BytecodeMethodKey, Map<String, Set<Int>>>()
return object : MethodVisitor(Opcodes.ASM5, null) {
override fun visitLineNumber(line: Int, start: Label?) {
if (start != null) {
labelsToAllStrings.getOrPutNullable(start.toString(), { LinkedHashSet<Int>() }).add(line)
}
ClassReader(bytes).accept(object : ClassVisitor(InlineCodegenUtil.API) {
override fun visitMethod(access: Int, name: String?, desc: String?, signature: String?, exceptions: Array<out String>?): MethodVisitor? {
if (name == null || desc == null) {
// TODO: check constructors
return null
}
val methodKey = BytecodeMethodKey(name, desc)
val methodLinesMapping = HashMap<String, MutableSet<Int>>()
lineNumberMapping[methodKey] = methodLinesMapping
return object : MethodVisitor(Opcodes.ASM5, null) {
override fun visitLineNumber(line: Int, start: Label?) {
if (start != null) {
methodLinesMapping.getOrPutNullable(start.toString(), { LinkedHashSet<Int>() }).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<KtFile, Int>? {
@@ -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
@@ -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>(
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)!! }
@@ -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<BinaryCacheKey, BytecodeDebugInfo?>() {
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<BinaryCacheKey, BytecodeDebugInfo?> {
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<BytecodeMethodKey, Map<String, Set<Int>>>)
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<KtFile, Int>? {
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
@@ -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<InlineFunctionHyperLinkInfo.InlineInfo>()