diff --git a/app/dist_files/Info.plist b/app/dist_files/Info.plist index 6a38897..81dc1a1 100644 --- a/app/dist_files/Info.plist +++ b/app/dist_files/Info.plist @@ -48,7 +48,7 @@ CFBundleVersion 100 NSHumanReadableCopyright - Copyright (C) 2020 + Copyright (C) 2024 NSHighResolutionCapable true diff --git a/app/src/main/java/com/github/grishberg/profiler/common/settings/JsonSettings.kt b/app/src/main/java/com/github/grishberg/profiler/common/settings/JsonSettings.kt index 682d727..b52e294 100644 --- a/app/src/main/java/com/github/grishberg/profiler/common/settings/JsonSettings.kt +++ b/app/src/main/java/com/github/grishberg/profiler/common/settings/JsonSettings.kt @@ -342,7 +342,6 @@ class JsonSettings( override fun filesDir() = filesDirName override var shouldShowToolbar: Boolean = false - override var lastVersion: String = "" private fun initWithDefaults() { initWithDefaultStringValue(SETTINGS_FONT_NAME, "Arial") diff --git a/core/build.gradle b/core/build.gradle index 98311cc..d9dba86 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -12,6 +12,7 @@ repositories { } dependencies { + implementation 'com.github.Grishberg:simpleperf-parser:1.0.6' implementation 'com.github.Grigory-Rylov:adb-facade-core:0.1.8' implementation 'com.github.Grigory-Rylov:andoid_method_trace_recorder:2.1.0' implementation 'com.github.Grigory-Rylov:proguard-deobfuscator:0.4.0' diff --git a/core/src/main/java/com/github/grishberg/profiler/analyzer/AnalyzerResultImpl.kt b/core/src/main/java/com/github/grishberg/profiler/analyzer/AnalyzerResultImpl.kt index a91b77c..1d4fee3 100644 --- a/core/src/main/java/com/github/grishberg/profiler/analyzer/AnalyzerResultImpl.kt +++ b/core/src/main/java/com/github/grishberg/profiler/analyzer/AnalyzerResultImpl.kt @@ -8,11 +8,14 @@ data class AnalyzerResultImpl( override val threadTimeBounds: Map, override val globalTimeBounds: Map, override val maxLevel: Int, - internal val mutableData: MutableMap>, + internal val mutableData: Map>, override val threads: List, override val mainThreadId: Int, override val startTimeUs: Long = -1, + override val minThreadTime: Double = 0.0, + override val minGlobalTime: Double = 0.0, ) : AnalyzerResult { + override val data: Map> get() = mutableData } diff --git a/core/src/main/java/com/github/grishberg/profiler/analyzer/ProfileDataImpl.kt b/core/src/main/java/com/github/grishberg/profiler/analyzer/ProfileDataImpl.kt index 6a744e9..480fa17 100644 --- a/core/src/main/java/com/github/grishberg/profiler/analyzer/ProfileDataImpl.kt +++ b/core/src/main/java/com/github/grishberg/profiler/analyzer/ProfileDataImpl.kt @@ -1,5 +1,6 @@ package com.github.grishberg.profiler.analyzer +import com.github.grishberg.profiler.core.ExtendedData import com.github.grishberg.profiler.core.ProfileData data class ProfileDataImpl( @@ -11,7 +12,8 @@ data class ProfileDataImpl( override var globalEndTimeInMillisecond: Double = -1.0, override var threadSelfTime: Double = 0.0, override var globalSelfTime: Double = 0.0, - override var parent: ProfileDataImpl? = null + override var parent: ProfileDataImpl? = null, + override var extendedData: ExtendedData? = null, ) : ProfileData { private val _children = mutableListOf() override val children: List = _children diff --git a/core/src/main/java/com/github/grishberg/profiler/analyzer/SimplePerfAnalyzer.kt b/core/src/main/java/com/github/grishberg/profiler/analyzer/SimplePerfAnalyzer.kt new file mode 100644 index 0000000..16fd162 --- /dev/null +++ b/core/src/main/java/com/github/grishberg/profiler/analyzer/SimplePerfAnalyzer.kt @@ -0,0 +1,159 @@ +package com.github.grishberg.profiler.analyzer + +import com.android.tools.profilers.cpu.CaptureNode +import com.android.tools.profilers.cpu.CpuCapture +import com.android.tools.profilers.cpu.CpuThreadInfo +import com.android.tools.profilers.cpu.nodemodel.CppFunctionModel +import com.android.tools.profilers.cpu.nodemodel.JavaMethodModel +import com.android.tools.profilers.cpu.nodemodel.NoSymbolModel +import com.android.tools.profilers.cpu.nodemodel.SyscallModel +import com.android.tools.profilers.cpu.simpleperf.SimpleperfTraceParser +import com.github.grishberg.profiler.analyzer.converter.NameConverter +import com.github.grishberg.profiler.common.AppLogger +import com.github.grishberg.profiler.core.AnalyzerResult +import com.github.grishberg.profiler.core.ExtendedData +import java.io.File +import kotlin.math.max +import kotlin.math.min +import kotlin.random.Random + +class SimplePerfAnalyzer( + private val log: AppLogger, + private val nameConverter: NameConverter, +) { + + fun analyze(traceFile: File): AnalyzerResult { + val parser = SimpleperfTraceParser() + val traceId = Random.nextLong() + try { + return captureToAnalyzerResult(parser.parse(traceFile, traceId)) + } catch (e: Exception) { + throw WrongFormatException() + } + } + + private fun captureToAnalyzerResult(capture: CpuCapture): AnalyzerResult { + val threads = mapThreads(capture.threads) + + val data = mutableMapOf>() + var maxLevel = 0 + val threadTimeBounds = mutableMapOf() + val globalTimeBounds = mutableMapOf() + + var minThreadTime = Long.MAX_VALUE + var minGlobalTime = Long.MAX_VALUE + + for (thread in threads) { + val currentThreadRoot = capture.getCaptureNode(thread.threadId) ?: continue + val profileDataList = mutableListOf() + for (threadChild in currentThreadRoot.children) { + val newRoot = mapCpuNode(threadChild) + val level = processChildren(profileDataList, threadChild, newRoot) + maxLevel = max(maxLevel, level) + } + threadTimeBounds[thread.threadId] = ThreadTimeBoundsImpl( + minTime = convertTime(currentThreadRoot.startThread), maxTime = convertTime(currentThreadRoot.endThread) + ) + globalTimeBounds[thread.threadId] = ThreadTimeBoundsImpl( + minTime = convertTime(currentThreadRoot.startGlobal), maxTime = convertTime(currentThreadRoot.endGlobal) + ) + + minThreadTime = min(minThreadTime, currentThreadRoot.startThread) + minGlobalTime = min(minGlobalTime, currentThreadRoot.startGlobal) + data[thread.threadId] = profileDataList + } + + return AnalyzerResultImpl( + threadTimeBounds = threadTimeBounds, + globalTimeBounds = globalTimeBounds, + maxLevel = maxLevel, + mutableData = data, + threads = threads, + mainThreadId = capture.mainThreadId, + startTimeUs = -1, + convertTime(minThreadTime), + convertTime(minGlobalTime), + ) + } + + private fun processChildren(allNodes: MutableList, node: CaptureNode, newRoot: ProfileDataImpl): Int { + var maxLevel = node.depth - 1 + node.children.forEachIndexed { index, captureNode -> + val profileDataChild = mapCpuNode(captureNode) + newRoot.addChild(profileDataChild) + allNodes.add(newRoot) + + val level = processChildren(allNodes, captureNode, profileDataChild) + maxLevel = max(maxLevel, level) + + } + return maxLevel + } + + private fun mapThreads(threads: Set): List { + return threads.map { ThreadItemImpl(it.name, it.id) }.sortedBy { it.threadId } + } + + private fun mapCpuNode(cpuNode: CaptureNode): ProfileDataImpl { + val data = cpuNode.data + val profileData = ProfileDataImpl( + name = data.fullName, + level = cpuNode.depth - THREAD_DEPTH_OFFSET, + threadStartTimeInMillisecond = convertTime(cpuNode.startThread), + threadEndTimeInMillisecond = convertTime(cpuNode.endThread), + globalStartTimeInMillisecond = convertTime(cpuNode.startGlobal), + globalEndTimeInMillisecond = convertTime(cpuNode.endGlobal), + ) + when (data) { + is CppFunctionModel -> { + profileData.extendedData = ExtendedData.CppFunctionData( + tag = data.tag, + id = data.id, + fullName = data.fullName, + classOrNamespace = data.classOrNamespace, + parameters = data.parameters, + isUserCode = data.isUserCode, + fileName = data.fileName, + vAddress = data.vAddress, + ) + } + + is JavaMethodModel -> { + profileData.extendedData = ExtendedData.JavaMethodData( + tag = data.tag, + id = data.id, + fullName = data.fullName, + className = data.className, + signature = data.signature, + ) + } + + is NoSymbolModel -> { + profileData.extendedData = ExtendedData.NoSymbolData( + tag = data.tag, + id = data.id, + fullName = data.fullName, + isKernel = data.isKernel, + ) + } + + is SyscallModel -> { + profileData.extendedData = ExtendedData.SyscallData( + tag = data.tag, + id = data.id, + fullName = data.fullName, + ) + } + } + + return profileData + } + + private fun convertTime(time: Long): Double { + return time.toDouble() / 1000.0 + } + + private companion object { + private const val THREAD_DEPTH_OFFSET = 1 + } +} diff --git a/core/src/main/java/com/github/grishberg/profiler/analyzer/TraceAnalyzer.kt b/core/src/main/java/com/github/grishberg/profiler/analyzer/TraceAnalyzer.kt index 1283e4e..7724350 100644 --- a/core/src/main/java/com/github/grishberg/profiler/analyzer/TraceAnalyzer.kt +++ b/core/src/main/java/com/github/grishberg/profiler/analyzer/TraceAnalyzer.kt @@ -1,255 +1,22 @@ package com.github.grishberg.profiler.analyzer -import com.android.tools.perflib.vmtrace.MethodInfo -import com.android.tools.perflib.vmtrace.TraceAction -import com.android.tools.perflib.vmtrace.VmTraceHandler -import com.android.tools.perflib.vmtrace.VmTraceParser +import com.github.grishberg.profiler.analyzer.converter.LegacyTraceAnalyzerTraceAnalyzer import com.github.grishberg.profiler.analyzer.converter.NameConverter import com.github.grishberg.profiler.analyzer.converter.NoOpNameConverter import com.github.grishberg.profiler.common.AppLogger +import com.github.grishberg.profiler.core.AnalyzerResult import java.io.File -import java.util.* -import kotlin.collections.ArrayList -import kotlin.collections.HashMap class TraceAnalyzer( private val log: AppLogger ) { var nameConverter: NameConverter = NoOpNameConverter - fun analyze(traceFile: File): AnalyzerResultImpl { - val vmTraceHandler = OnVmTraceHandler(log, nameConverter) - VmTraceParser(traceFile, vmTraceHandler).parse() - - for (threadId in vmTraceHandler.threads) { - val traceDataForThread = vmTraceHandler.traceData.getOrDefault(threadId.key, mutableListOf()) - - for (duration in traceDataForThread) { - if (duration.threadEndTimeInMillisecond == -1.0) { - duration.threadEndTimeInMillisecond = - vmTraceHandler.threadTimeBounds.getOrDefault(threadId.key, ThreadTimeBoundsImpl()).maxTime - } - if (duration.globalEndTimeInMillisecond == -1.0) { - duration.globalEndTimeInMillisecond = - vmTraceHandler.globalTimeBounds.getOrDefault(threadId.key, ThreadTimeBoundsImpl()).maxTime - } - } - - for (duration in traceDataForThread) { - duration.updateSelfTime() - } - } - - val sortedThreads = ArrayList() - - var threadIndex = 0 - for (threadEntity in vmTraceHandler.globalTimeBounds) { - val id = threadEntity.key - if (id == vmTraceHandler.mainThreadId) { - continue - } - - val thread = vmTraceHandler.threads[id] - val threadName = thread ?: "Thread-$threadIndex" - - sortedThreads.add(ThreadItemImpl(threadName, id)) - threadIndex++ - } - - sortedThreads.sortBy { it.name } - - sortedThreads.add( - 0, - ThreadItemImpl( - vmTraceHandler.threads.getOrDefault(vmTraceHandler.mainThreadId, "main")!!, - vmTraceHandler.mainThreadId - ) - ) - - return AnalyzerResultImpl( - vmTraceHandler.threadTimeBounds, - vmTraceHandler.globalTimeBounds, - vmTraceHandler.maxLevel, - vmTraceHandler.traceData, - sortedThreads, - vmTraceHandler.mainThreadId, - vmTraceHandler.startTime - ) - } - - private fun updateSelfTime(current: ProfileDataImpl) { - if (current.threadSelfTime > 0.0 && current.globalSelfTime > 0.0) { - return - } - current.apply { - threadSelfTime = threadEndTimeInMillisecond - threadStartTimeInMillisecond - globalSelfTime = globalEndTimeInMillisecond - globalStartTimeInMillisecond - for (child in children) { - threadSelfTime -= child.threadEndTimeInMillisecond - child.threadStartTimeInMillisecond - globalSelfTime -= child.globalEndTimeInMillisecond - child.globalStartTimeInMillisecond - try { - updateSelfTime(child) - } catch (e: StackOverflowError) { - e.printStackTrace() - } - } - } - } - - internal class OnVmTraceHandler( - private val log: AppLogger, - private val nameConverter: NameConverter - ) : VmTraceHandler { - private var version: Int = -1 - - var mainThreadIndex = 0 - val threads = mutableMapOf() - - val properties = mutableMapOf() - - val methodsAndClasses = MethodsAndClasses() - val methods = mutableMapOf() - - val threadTimeBounds = mutableMapOf() - val globalTimeBounds = mutableMapOf() - var mainThreadId = -1 - var maxLevel = 0 - - val traceData = mutableMapOf>() - val methodsStacksForThread = mutableMapOf>>() - val level = mutableMapOf() - val parents = mutableMapOf>() - var startTime: Long = -1 - - override fun setVersion(version: Int) { - this.version = version - } - - override fun setProperty(key: String?, value: String?) { - properties[key] = value - } - - override fun addThread(id: Int, name: String?) { - threads[id] = name - if (name == "main") { - mainThreadId = id - mainThreadIndex = threads.size - 1 - } - } - - override fun addMethod(id: Long, info: MethodInfo?) { - methods[id] = info - info?.let { - methodsAndClasses.put(id, MethodData(it.fullName, it.className, it.methodName)) - } - } - - override fun addMethodAction( - threadId: Int, - methodId: Long, - methodAction: TraceAction, - threadTime: Int, - globalTime: Int - ) { - if (methods[methodId] == null) { - return - } - val methodInfo = methods[methodId]!! - val thread = threads[threadId] - - val threadTimeBoundsForThread = threadTimeBounds.getOrPut(threadId) { ThreadTimeBoundsImpl() } - val globalTimeBoundsForThread = globalTimeBounds.getOrPut(threadId) { ThreadTimeBoundsImpl() } - - if (level[threadId] == null) { - level[threadId] = 0 - } - if (methodAction != TraceAction.METHOD_ENTER) { - val parentsStackForThread = parents.getOrDefault(threadId, Stack()) - val stacksForThread = methodsStacksForThread.getOrDefault(threadId, HashMap()) - - val stack = stacksForThread[methodId] - if (stack == null) { - log.d("There are no any stack for methodId=${methodId}, thread=$thread, startTime=$globalTime") - } else { - if (parentsStackForThread.isNotEmpty()) { - parentsStackForThread.pop() - } - if (stack.isNotEmpty()) { - val data = stack.pop() - data.apply { - threadEndTimeInMillisecond = threadTime / 1000.0 - globalEndTimeInMillisecond = globalTime / 1000.0 - } - if (threadTimeBoundsForThread.maxTime < threadTime / 1000.0) { - threadTimeBoundsForThread.maxTime = threadTime / 1000.0 - } - if (globalTimeBoundsForThread.maxTime < globalTime / 1000.0) { - globalTimeBoundsForThread.maxTime = globalTime / 1000.0 - } - level[threadId] = level[threadId]!! - 1 - } else { - log.w("Action $methodAction but stack is empty for thread=$threadId, startTime=$globalTime\"") - } - } - } - - if (methodAction == TraceAction.METHOD_ENTER) { - var parentsStackForThread = parents[threadId] - if (parentsStackForThread == null) { - parentsStackForThread = Stack() - parents[threadId] = parentsStackForThread - } - - var stacksForThread: MutableMap>? = methodsStacksForThread[threadId] - if (stacksForThread == null) { - stacksForThread = HashMap() - methodsStacksForThread[threadId] = stacksForThread - } - - var stack: Stack? = stacksForThread[methodId] - if (stack == null) { - stack = Stack() - stacksForThread[methodId] = stack - } - val parent: ProfileDataImpl? = - if (parentsStackForThread.isEmpty()) null else parentsStackForThread.peek() - - val convertedClassName = nameConverter.convertClassName(methodInfo.className) - val convertedMethodName = - nameConverter.convertMethodName(convertedClassName, methodInfo.methodName, methodInfo.signature) - val duration = ProfileDataImpl( - "${convertedClassName}.${convertedMethodName}", - level.getOrDefault(threadId, -1), - threadStartTimeInMillisecond = threadTime / 1000.0, - globalStartTimeInMillisecond = globalTime / 1000.0, - parent = parent - ) - parent?.addChild(duration) - - var traceDataForThread = traceData[threadId] - if (traceDataForThread == null) { - traceDataForThread = ArrayList() - traceData[threadId] = traceDataForThread - } - traceDataForThread.add(duration) - stack.push(duration) - parentsStackForThread.push(duration) - if (threadTimeBoundsForThread.minTime > threadTime / 1000.0) { - threadTimeBoundsForThread.minTime = threadTime / 1000.0 - } - if (globalTimeBoundsForThread.minTime > threadTime / 1000.0) { - globalTimeBoundsForThread.minTime = threadTime / 1000.0 - } - - if (maxLevel < level[threadId]!!) { - maxLevel = level[threadId]!! - } - level[threadId] = level[threadId]!! + 1 - } - } - - override fun setStartTimeUs(startTimeUs: Long) { - this.startTime = startTimeUs + fun analyze(traceFile: File): AnalyzerResult { + return try { + SimplePerfAnalyzer(log, nameConverter).analyze(traceFile) + } catch (e: WrongFormatException){ + LegacyTraceAnalyzerTraceAnalyzer(log, nameConverter).analyze(traceFile) } } } diff --git a/core/src/main/java/com/github/grishberg/profiler/analyzer/WrongFormatException.kt b/core/src/main/java/com/github/grishberg/profiler/analyzer/WrongFormatException.kt new file mode 100644 index 0000000..9f0e119 --- /dev/null +++ b/core/src/main/java/com/github/grishberg/profiler/analyzer/WrongFormatException.kt @@ -0,0 +1,3 @@ +package com.github.grishberg.profiler.analyzer + +class WrongFormatException:Exception() diff --git a/core/src/main/java/com/github/grishberg/profiler/analyzer/converter/LegacyTraceAnalyzer.kt b/core/src/main/java/com/github/grishberg/profiler/analyzer/converter/LegacyTraceAnalyzer.kt new file mode 100644 index 0000000..edfffde --- /dev/null +++ b/core/src/main/java/com/github/grishberg/profiler/analyzer/converter/LegacyTraceAnalyzer.kt @@ -0,0 +1,258 @@ +package com.github.grishberg.profiler.analyzer.converter + +import com.android.tools.perflib.vmtrace.MethodInfo +import com.android.tools.perflib.vmtrace.TraceAction +import com.android.tools.perflib.vmtrace.VmTraceHandler +import com.android.tools.perflib.vmtrace.VmTraceParser +import com.github.grishberg.profiler.analyzer.AnalyzerResultImpl +import com.github.grishberg.profiler.analyzer.MethodData +import com.github.grishberg.profiler.analyzer.MethodsAndClasses +import com.github.grishberg.profiler.analyzer.ProfileDataImpl +import com.github.grishberg.profiler.analyzer.ThreadItemImpl +import com.github.grishberg.profiler.analyzer.ThreadTimeBoundsImpl +import com.github.grishberg.profiler.common.AppLogger +import java.io.File +import java.util.Stack + +class LegacyTraceAnalyzerTraceAnalyzer( + private val log: AppLogger, + private val nameConverter: NameConverter = NoOpNameConverter +) { + + fun analyze(traceFile: File): AnalyzerResultImpl { + val vmTraceHandler = OnVmTraceHandler(log, nameConverter) + VmTraceParser(traceFile, vmTraceHandler).parse() + + for (threadId in vmTraceHandler.threads) { + val traceDataForThread = vmTraceHandler.traceData.getOrDefault(threadId.key, mutableListOf()) + + for (duration in traceDataForThread) { + if (duration.threadEndTimeInMillisecond == -1.0) { + duration.threadEndTimeInMillisecond = + vmTraceHandler.threadTimeBounds.getOrDefault(threadId.key, ThreadTimeBoundsImpl()).maxTime + } + if (duration.globalEndTimeInMillisecond == -1.0) { + duration.globalEndTimeInMillisecond = + vmTraceHandler.globalTimeBounds.getOrDefault(threadId.key, ThreadTimeBoundsImpl()).maxTime + } + } + + for (duration in traceDataForThread) { + duration.updateSelfTime() + } + } + + val sortedThreads = ArrayList() + + var threadIndex = 0 + for (threadEntity in vmTraceHandler.globalTimeBounds) { + val id = threadEntity.key + if (id == vmTraceHandler.mainThreadId) { + continue + } + + val thread = vmTraceHandler.threads[id] + val threadName = thread ?: "Thread-$threadIndex" + + sortedThreads.add(ThreadItemImpl(threadName, id)) + threadIndex++ + } + + sortedThreads.sortBy { it.name } + + sortedThreads.add( + 0, + ThreadItemImpl( + vmTraceHandler.threads.getOrDefault(vmTraceHandler.mainThreadId, "main")!!, + vmTraceHandler.mainThreadId + ) + ) + + return AnalyzerResultImpl( + vmTraceHandler.threadTimeBounds, + vmTraceHandler.globalTimeBounds, + vmTraceHandler.maxLevel, + vmTraceHandler.traceData, + sortedThreads, + vmTraceHandler.mainThreadId, + vmTraceHandler.startTime + ) + } + + private fun updateSelfTime(current: ProfileDataImpl) { + if (current.threadSelfTime > 0.0 && current.globalSelfTime > 0.0) { + return + } + current.apply { + threadSelfTime = threadEndTimeInMillisecond - threadStartTimeInMillisecond + globalSelfTime = globalEndTimeInMillisecond - globalStartTimeInMillisecond + for (child in children) { + threadSelfTime -= child.threadEndTimeInMillisecond - child.threadStartTimeInMillisecond + globalSelfTime -= child.globalEndTimeInMillisecond - child.globalStartTimeInMillisecond + try { + updateSelfTime(child) + } catch (e: StackOverflowError) { + e.printStackTrace() + } + } + } + } + + internal class OnVmTraceHandler( + private val log: AppLogger, + private val nameConverter: NameConverter + ) : VmTraceHandler { + private var version: Int = -1 + + var mainThreadIndex = 0 + val threads = mutableMapOf() + + val properties = mutableMapOf() + + val methodsAndClasses = MethodsAndClasses() + val methods = mutableMapOf() + + val threadTimeBounds = mutableMapOf() + val globalTimeBounds = mutableMapOf() + var mainThreadId = -1 + var maxLevel = 0 + + val traceData = mutableMapOf>() + val methodsStacksForThread = mutableMapOf>>() + val level = mutableMapOf() + val parents = mutableMapOf>() + var startTime: Long = -1 + + override fun setVersion(version: Int) { + this.version = version + } + + override fun setProperty(key: String?, value: String?) { + properties[key] = value + } + + override fun addThread(id: Int, name: String?) { + threads[id] = name + if (name == "main") { + mainThreadId = id + mainThreadIndex = threads.size - 1 + } + } + + override fun addMethod(id: Long, info: MethodInfo?) { + methods[id] = info + info?.let { + methodsAndClasses.put(id, MethodData(it.fullName, it.className, it.methodName)) + } + } + + override fun addMethodAction( + threadId: Int, + methodId: Long, + methodAction: TraceAction, + threadTime: Int, + globalTime: Int + ) { + if (methods[methodId] == null) { + return + } + val methodInfo = methods[methodId]!! + val thread = threads[threadId] + + val threadTimeBoundsForThread = threadTimeBounds.getOrPut(threadId) { ThreadTimeBoundsImpl() } + val globalTimeBoundsForThread = globalTimeBounds.getOrPut(threadId) { ThreadTimeBoundsImpl() } + + if (level[threadId] == null) { + level[threadId] = 0 + } + if (methodAction != TraceAction.METHOD_ENTER) { + val parentsStackForThread = parents.getOrDefault(threadId, Stack()) + val stacksForThread = methodsStacksForThread.getOrDefault(threadId, HashMap()) + + val stack = stacksForThread[methodId] + if (stack == null) { + log.d("There are no any stack for methodId=${methodId}, thread=$thread, startTime=$globalTime") + } else { + if (parentsStackForThread.isNotEmpty()) { + parentsStackForThread.pop() + } + if (stack.isNotEmpty()) { + val data = stack.pop() + data.apply { + threadEndTimeInMillisecond = threadTime / 1000.0 + globalEndTimeInMillisecond = globalTime / 1000.0 + } + if (threadTimeBoundsForThread.maxTime < threadTime / 1000.0) { + threadTimeBoundsForThread.maxTime = threadTime / 1000.0 + } + if (globalTimeBoundsForThread.maxTime < globalTime / 1000.0) { + globalTimeBoundsForThread.maxTime = globalTime / 1000.0 + } + level[threadId] = level[threadId]!! - 1 + } else { + log.w("Action $methodAction but stack is empty for thread=$threadId, startTime=$globalTime\"") + } + } + } + + if (methodAction == TraceAction.METHOD_ENTER) { + var parentsStackForThread = parents[threadId] + if (parentsStackForThread == null) { + parentsStackForThread = Stack() + parents[threadId] = parentsStackForThread + } + + var stacksForThread: MutableMap>? = methodsStacksForThread[threadId] + if (stacksForThread == null) { + stacksForThread = HashMap() + methodsStacksForThread[threadId] = stacksForThread + } + + var stack: Stack? = stacksForThread[methodId] + if (stack == null) { + stack = Stack() + stacksForThread[methodId] = stack + } + val parent: ProfileDataImpl? = + if (parentsStackForThread.isEmpty()) null else parentsStackForThread.peek() + + val convertedClassName = nameConverter.convertClassName(methodInfo.className) + val convertedMethodName = + nameConverter.convertMethodName(convertedClassName, methodInfo.methodName, methodInfo.signature) + val duration = ProfileDataImpl( + "${convertedClassName}.${convertedMethodName}", + level.getOrDefault(threadId, -1), + threadStartTimeInMillisecond = threadTime / 1000.0, + globalStartTimeInMillisecond = globalTime / 1000.0, + parent = parent + ) + parent?.addChild(duration) + + var traceDataForThread = traceData[threadId] + if (traceDataForThread == null) { + traceDataForThread = ArrayList() + traceData[threadId] = traceDataForThread + } + traceDataForThread.add(duration) + stack.push(duration) + parentsStackForThread.push(duration) + if (threadTimeBoundsForThread.minTime > threadTime / 1000.0) { + threadTimeBoundsForThread.minTime = threadTime / 1000.0 + } + if (globalTimeBoundsForThread.minTime > threadTime / 1000.0) { + globalTimeBoundsForThread.minTime = threadTime / 1000.0 + } + + if (maxLevel < level[threadId]!!) { + maxLevel = level[threadId]!! + } + level[threadId] = level[threadId]!! + 1 + } + } + + override fun setStartTimeUs(startTimeUs: Long) { + this.startTime = startTimeUs + } + } +} + diff --git a/core/src/main/java/com/github/grishberg/profiler/chart/CallTracePanel.java b/core/src/main/java/com/github/grishberg/profiler/chart/CallTracePanel.java index 9fd5865..941c59e 100644 --- a/core/src/main/java/com/github/grishberg/profiler/chart/CallTracePanel.java +++ b/core/src/main/java/com/github/grishberg/profiler/chart/CallTracePanel.java @@ -14,25 +14,13 @@ import com.github.grishberg.profiler.comparator.model.ComparableProfileData; import com.github.grishberg.profiler.core.AnalyzerResult; import com.github.grishberg.profiler.core.ProfileData; +import com.github.grishberg.profiler.core.ThreadTimeBounds; import com.github.grishberg.profiler.ui.BookMarkInfo; import com.github.grishberg.profiler.ui.SimpleComponentListener; import com.github.grishberg.profiler.ui.TimeFormatter; import com.github.grishberg.profiler.ui.ZoomAndPanDelegate; import com.github.grishberg.profiler.ui.theme.Palette; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import javax.swing.JPanel; -import java.awt.Color; -import java.awt.Dimension; -import java.awt.Font; -import java.awt.FontMetrics; -import java.awt.Graphics; -import java.awt.Graphics2D; -import java.awt.Point; -import java.awt.Rectangle; -import java.awt.RenderingHints; -import java.awt.Shape; +import java.awt.*; import java.awt.event.ComponentEvent; import java.awt.event.MouseEvent; import java.awt.geom.AffineTransform; @@ -46,15 +34,30 @@ import java.util.Iterator; import java.util.List; import java.util.Map; +import javax.swing.*; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class CallTracePanel extends JPanel + implements ProfileDataDimensionDelegate, ChartPaintDelegate, RepaintDelegate { -public class CallTracePanel extends JPanel implements ProfileDataDimensionDelegate, ChartPaintDelegate, RepaintDelegate { public static final int TOP_OFFSET = 20; public static final int MARKER_LABEL_TEXT_MIN_WIDTH = 20; private static final int FIT_PADDING = 80; private static final int SCALE_FONT_SIZE = 13; private static final double NOT_FOUND_ITEM_DARKEN_FACTOR = 0.5; private static final double MINIMUM_WIDTH_IN_PX = 1; - private static final AnalyzerResultImpl RESULT_STUB = new AnalyzerResultImpl(Collections.emptyMap(), Collections.emptyMap(), 0, Collections.emptyMap(), Collections.emptyList(), 0, -1); + private static final AnalyzerResultImpl RESULT_STUB = + new AnalyzerResultImpl(Collections.emptyMap(), + Collections.emptyMap(), + 0, + Collections.emptyMap(), + Collections.emptyList(), + 0, + -1, + 0.0, + 0.0 + ); private final FoundNavigationListener foundNavigationListener; private boolean init = true; @@ -68,8 +71,8 @@ public class CallTracePanel extends JPanel implements ProfileDataDimensionDelega private int leftSymbolOffset = 4; private int fontTopOffset = 4; private double maxRightOffset; + private double minLeftOffset; private double maxBottomOffset; - private double minTime; private int minLevel; private Dimension screenSize; @@ -108,18 +111,20 @@ public class CallTracePanel extends JPanel implements ProfileDataDimensionDelega private final MethodsNameDrawer cellPaintDelegate = new MethodsNameDrawer(leftSymbolOffset); private final ElementColor colorBuffer = new ElementColor(); - public CallTracePanel(TimeFormatter timeFormatter, - MethodsColorImpl methodsColor, - FoundNavigationListener foundInfoListener, - SettingsFacade settings, - AppLogger logger, - DependenciesFoundAction dependenciesFoundAction, - StagesFacade stagesFacade, - SystraceStagesFacade systraceStagesFacade, - Bookmarks bookmarks, - PreviewImageRepository previewImageRepository, - CallTracePreviewPanel previewPanel, - Palette palette) { + public CallTracePanel( + TimeFormatter timeFormatter, + MethodsColorImpl methodsColor, + FoundNavigationListener foundInfoListener, + SettingsFacade settings, + AppLogger logger, + DependenciesFoundAction dependenciesFoundAction, + StagesFacade stagesFacade, + SystraceStagesFacade systraceStagesFacade, + Bookmarks bookmarks, + PreviewImageRepository previewImageRepository, + CallTracePreviewPanel previewPanel, + Palette palette + ) { this.timeFormatter = timeFormatter; this.methodsColor = methodsColor; this.foundNavigationListener = foundInfoListener; @@ -130,7 +135,8 @@ public CallTracePanel(TimeFormatter timeFormatter, this.previewImageRepository = previewImageRepository; this.previewPanel = previewPanel; this.palette = palette; - this.zoomAndPanDelegate = new ZoomAndPanDelegate(this, TOP_OFFSET, new ZoomAndPanDelegate.LeftTopBounds()); + this.zoomAndPanDelegate = + new ZoomAndPanDelegate(this, TOP_OFFSET, new ZoomAndPanDelegate.LeftTopBounds()); this.bookmarks = bookmarks; stagesFacade.setRepaintDelegate(this); stagesFacade.setLabelPaintDelegate(this); @@ -185,9 +191,11 @@ public void onPreviewClicked(double offsetInPercent) { double offset = 0; if (isThreadTime) { - offset = result.getThreadTimeBounds().get(currentThreadId).getMaxTime() * offsetInPercent; + offset = result.getThreadTimeBounds().get(currentThreadId).getMaxTime() * + offsetInPercent; } else { - offset = result.getGlobalTimeBounds().get(currentThreadId).getMaxTime() * offsetInPercent; + offset = result.getGlobalTimeBounds().get(currentThreadId).getMaxTime() * + offsetInPercent; } zoomAndPanDelegate.scrollTo(offset); } @@ -198,10 +206,14 @@ public void updatePreviewImage() { if (result == RESULT_STUB) { return; } - PreviewType previewType = isThreadTime ? PreviewType.PREVIEW_THREAD : PreviewType.PREVIEW_GLOBAL; - BufferedImage cachedImage = previewImageRepository.preparePreview(currentThreadId, previewType, (image, threadId) -> { - previewPanel.setImage(image); - }); + PreviewType previewType = + isThreadTime ? PreviewType.PREVIEW_THREAD : PreviewType.PREVIEW_GLOBAL; + BufferedImage cachedImage = previewImageRepository.preparePreview(currentThreadId, + previewType, + (image, threadId) -> { + previewPanel.setImage(image); + } + ); if (cachedImage != null) { previewPanel.setImage(cachedImage); } @@ -215,10 +227,17 @@ private boolean checkBookmarkHeaderClicked(Point point) { Rectangle rect = transformedShape.getBounds(); int cx = (rect.x + rect.width / 2); - int labelTextWidth = Math.max(labelFontMetrics.stringWidth(bookmark.getName()), MARKER_LABEL_TEXT_MIN_WIDTH); + int labelTextWidth = Math.max( + labelFontMetrics.stringWidth(bookmark.getName()), + MARKER_LABEL_TEXT_MIN_WIDTH + ); // header background - Rectangle labelRect = new Rectangle(cx - labelTextWidth / 2 - leftSymbolOffset, 0, labelTextWidth + 2 * leftSymbolOffset, TOP_OFFSET); + Rectangle labelRect = new Rectangle(cx - labelTextWidth / 2 - leftSymbolOffset, + 0, + labelTextWidth + 2 * leftSymbolOffset, + TOP_OFFSET + ); if (labelRect.contains(point)) { if (rightClickListener != null) { rightClickListener.onBookmarkRightClicked(point.x, point.y, bookmark); @@ -230,34 +249,35 @@ private boolean checkBookmarkHeaderClicked(Point point) { } public void setMouseEventListener(ZoomAndPanDelegate.MouseEventsListener l) { - ZoomAndPanDelegate.MouseEventsListener delegate = new ZoomAndPanDelegate.MouseEventsListener() { - @Override - public void onMouseClicked(Point point, double x, double y) { - l.onMouseClicked(point, x, y); - } + ZoomAndPanDelegate.MouseEventsListener delegate = + new ZoomAndPanDelegate.MouseEventsListener() { + @Override + public void onMouseClicked(Point point, double x, double y) { + l.onMouseClicked(point, x, y); + } - @Override - public void onMouseMove(Point point, double x, double y) { - l.onMouseMove(point, x, y); - } + @Override + public void onMouseMove(Point point, double x, double y) { + l.onMouseMove(point, x, y); + } - @Override - public void onMouseExited() { - l.onMouseExited(); - } + @Override + public void onMouseExited() { + l.onMouseExited(); + } - @Override - public void onControlMouseClicked(Point point, double x, double y) { - l.onControlMouseClicked(point, x, y); - findCreatedByDagger(x, y); - } + @Override + public void onControlMouseClicked(Point point, double x, double y) { + l.onControlMouseClicked(point, x, y); + findCreatedByDagger(x, y); + } - @Override - public void onControlShiftMouseClicked(Point point, double x, double y) { - l.onControlShiftMouseClicked(point, x, y); - findDaggerCaller(x, y); - } - }; + @Override + public void onControlShiftMouseClicked(Point point, double x, double y) { + l.onControlShiftMouseClicked(point, x, y); + findDaggerCaller(x, y); + } + }; zoomAndPanDelegate.setMouseEventsListener(delegate); } @@ -303,14 +323,17 @@ public void openTraceResult(TraceContainer trace) { switchThread(result.getMainThreadId()); this.minLevel = 0; - this.minTime = 0; maxBottomOffset = calculateTopForLevel(result.getMaxLevel()) + levelHeight; bookmarks.set(trace.getBookmarks()); bookmarks.setup(maxBottomOffset, isThreadTime); zoomAndPanDelegate.setTransform(new AffineTransform()); - zoomAndPanDelegate.fitZoom(new Rectangle.Double(0, 0, maxRightOffset, maxBottomOffset), 0, ZoomAndPanDelegate.VerticalAlign.NONE); + zoomAndPanDelegate.fitZoom( + new Rectangle.Double(minLeftOffset, 0, maxRightOffset - minLeftOffset, maxBottomOffset), + 0, + ZoomAndPanDelegate.VerticalAlign.NONE + ); removeSelection(); previewImageRepository.setAnalyzerResult(result); updatePreviewImage(); @@ -362,7 +385,9 @@ public void highlightCompare(ComparableProfileData rootCompareData, int threadId updateStages(threadId, objectsForThread); - if (rootCompareData.getProfileData().getLevel() != -1) throw new IllegalStateException("Root has level -1"); + if (rootCompareData.getProfileData().getLevel() != -1) { + throw new IllegalStateException("Root has level -1"); + } rebuildDataWithCompare(rootCompareData, objectsForThread); repaint(); } @@ -378,7 +403,10 @@ public void updateCompare(ComparableProfileData root, int threadId) { repaint(); } - private void updateCompare(ComparableProfileData root, Map objectsForThread) { + private void updateCompare( + ComparableProfileData root, + Map objectsForThread + ) { ProfileRectangle rect = createProfileRectangle(root.getProfileData()); ProfileRectangle currentRect = objectsForThread.get(rect.toString()); currentRect.setColor(methodsColor.getColorForCompare(root.getMark(), root.getName())); @@ -387,7 +415,10 @@ private void updateCompare(ComparableProfileData root, Map objectsForThread) { + private void rebuildDataWithCompare( + ComparableProfileData root, + List objectsForThread + ) { if (root.getProfileData().getLevel() != -1) { ProfileRectangle rect = createProfileRectangle(root.getProfileData()); rect.setColor(methodsColor.getColorForCompare(root.getMark(), root.getName())); @@ -399,23 +430,33 @@ private void rebuildDataWithCompare(ComparableProfileData root, List objectsForThread) { - stagesFacade.onThreadSwitched(objectsForThread, - threadId == result.getMainThreadId(), - isThreadTime, - TOP_OFFSET); - systraceStagesFacade.onThreadSwitched(objectsForThread, - threadId == result.getMainThreadId(), - isThreadTime, - TOP_OFFSET); + stagesFacade.onThreadSwitched( + objectsForThread, + threadId == result.getMainThreadId(), + isThreadTime, + TOP_OFFSET + ); + systraceStagesFacade.onThreadSwitched( + objectsForThread, + threadId == result.getMainThreadId(), + isThreadTime, + TOP_OFFSET + ); } private void updateBounds(int threadId) { if (isThreadTime) { - maxRightOffset = result.getThreadTimeBounds().getOrDefault(threadId, new ThreadTimeBoundsImpl()).getMaxTime(); + ThreadTimeBounds threadTimeBounds = result.getThreadTimeBounds() + .getOrDefault(threadId, new ThreadTimeBoundsImpl()); + maxRightOffset = threadTimeBounds.getMaxTime(); + minLeftOffset = threadTimeBounds.getMinTime(); } else { - maxRightOffset = result.getGlobalTimeBounds().getOrDefault(threadId, new ThreadTimeBoundsImpl()).getMaxTime(); + ThreadTimeBounds threadTimeBounds = result.getGlobalTimeBounds() + .getOrDefault(threadId, new ThreadTimeBoundsImpl()); + maxRightOffset = threadTimeBounds.getMaxTime(); + minLeftOffset = threadTimeBounds.getMinTime(); } - zoomAndPanDelegate.updateRightBottomCorner(maxBottomOffset, maxBottomOffset); + zoomAndPanDelegate.updateBounds(minLeftOffset, maxRightOffset, maxBottomOffset); } private void rebuildData(List objectsForThread) { @@ -438,11 +479,12 @@ private ProfileRectangle createProfileRectangle(ProfileData record) { double width = right - left; return new ProfileRectangle( - left, - top, - width, - levelHeight, - record); + left, + top, + width, + levelHeight, + record + ); } private void updateData() { @@ -517,8 +559,9 @@ protected void paintComponent(Graphics graphics) { private void draw(Graphics2D g) { g.setRenderingHint( - RenderingHints.KEY_TEXT_ANTIALIASING, - RenderingHints.VALUE_TEXT_ANTIALIAS_GASP); + RenderingHints.KEY_TEXT_ANTIALIASING, + RenderingHints.VALUE_TEXT_ANTIALIAS_GASP + ); int fontSize = settings.getFontSize(); fontName = "Arial"; @@ -536,12 +579,25 @@ private void paintObjects(Graphics2D g, AffineTransform at) { double screenTop = 0; double screenRight = 0; double screenBottom = 0; + if (result != null) { + Map bounds; + if (isThreadTime) { + bounds = result.getThreadTimeBounds(); + } else { + bounds = result.getGlobalTimeBounds(); + } + ThreadTimeBounds currentBound = bounds.get(currentThreadId); + if (currentBound != null) { + screenLeft = currentBound.getMinTime(); + } + } g.setColor(palette.getTraceBackgroundColor()); g.fillRect(0, 0, getWidth(), getHeight()); try { Point2D.Double leftTop = zoomAndPanDelegate.transformPoint(new Point(0, 0)); - Point2D.Double rightBottom = zoomAndPanDelegate.transformPoint(new Point(screenSize.width, screenSize.height)); + Point2D.Double rightBottom = + zoomAndPanDelegate.transformPoint(new Point(screenSize.width, screenSize.height)); screenLeft = leftTop.x; screenTop = leftTop.y; @@ -558,7 +614,8 @@ private void paintObjects(Graphics2D g, AffineTransform at) { FontMetrics fm = getFontMetrics(g.getFont()); - List objectsForThread = objects.getOrDefault(currentThreadId, Collections.emptyList()); + List objectsForThread = + objects.getOrDefault(currentThreadId, Collections.emptyList()); // draw rectangles for (int i = 0; i < objectsForThread.size(); i++) { @@ -599,13 +656,26 @@ private void paintObjects(Graphics2D g, AffineTransform at) { double left = Math.max(0, bounds.x); double right = Math.min(screenSize.width, bounds.x + bounds.width); cellPaintDelegate.drawLabel(g, fm, element.profileData.getName(), - left, right, bounds.y + bounds.height - fontTopOffset); + left, right, bounds.y + bounds.height - fontTopOffset + ); } // draw selections @Nullable - ProfileRectangle selected = currentSelectedElement >= 0 ? objectsForThread.get(currentSelectedElement) : null; - calledStacktrace.draw(g, at, fm, currentThreadId, selected, minimumSizeInMs, screenLeft, screenTop, screenRight, screenBottom); + ProfileRectangle selected = + currentSelectedElement >= 0 ? objectsForThread.get(currentSelectedElement) : null; + calledStacktrace.draw( + g, + at, + fm, + currentThreadId, + selected, + minimumSizeInMs, + screenLeft, + screenTop, + screenRight, + screenBottom + ); // toolbar background. g.setColor(toolbarColor); @@ -658,25 +728,37 @@ private void drawToolbar(Graphics2D g, AffineTransform at, FontMetrics fm) { Rectangle rect = transformedShape.getBounds(); int cx = (rect.x + rect.width / 2); - int labelTextWidth = Math.max(fm.stringWidth(bookmark.getName()), MARKER_LABEL_TEXT_MIN_WIDTH); + int labelTextWidth = + Math.max(fm.stringWidth(bookmark.getName()), MARKER_LABEL_TEXT_MIN_WIDTH); // header background g.setColor(bookmark.getHeaderColor()); - g.fillRect(cx - labelTextWidth / 2 - leftSymbolOffset, 0, labelTextWidth + 2 * leftSymbolOffset, TOP_OFFSET); + g.fillRect( + cx - labelTextWidth / 2 - leftSymbolOffset, + 0, + labelTextWidth + 2 * leftSymbolOffset, + TOP_OFFSET + ); g.setColor(bookmark.getHeaderTitleColor()); if (bookmark.getName().length() > 0) { - g.drawString(bookmark.getName(), cx - labelTextWidth / 2, labelFontMetrics.getHeight()); + g.drawString( + bookmark.getName(), + cx - labelTextWidth / 2, + labelFontMetrics.getHeight() + ); } } } @Override - public void drawLabel(Graphics2D g, - FontMetrics fm, - String name, - Rectangle horizontalBounds, - int topPosition) { + public void drawLabel( + Graphics2D g, + FontMetrics fm, + String name, + Rectangle horizontalBounds, + int topPosition + ) { int leftPosition = horizontalBounds.x + leftSymbolOffset; if (leftPosition < 0) { leftPosition = 0; @@ -696,13 +778,16 @@ public void drawLabel(Graphics2D g, } private void calculateColorProfileData( - ProfileRectangle element, - List objects) { - boolean isSelectedElement = currentSelectedElement >= 0 && element == objects.get(currentSelectedElement); + ProfileRectangle element, + List objects + ) { + boolean isSelectedElement = + currentSelectedElement >= 0 && element == objects.get(currentSelectedElement); if (isSearchingInProgress) { if (element.isFoundElement) { - boolean isFocusedElement = currentFocusedFoundElement >= 0 && element == foundItems.get(currentFocusedFoundElement); + boolean isFocusedElement = currentFocusedFoundElement >= 0 && + element == foundItems.get(currentFocusedFoundElement); Color color = isSelectedElement ? selectedFoundColor : foundColor; if (isFocusedElement && !isSelectedElement) { color = focusedFoundColor; @@ -731,10 +816,12 @@ private void calculateColorProfileData( } public static Color darker(Color color, Double darkenFactor) { - return new Color(Math.max((int) (color.getRed() * darkenFactor), 0), - Math.max((int) (color.getGreen() * darkenFactor), 0), - Math.max((int) (color.getBlue() * darkenFactor), 0), - color.getAlpha()); + return new Color( + Math.max((int) (color.getRed() * darkenFactor), 0), + Math.max((int) (color.getGreen() * darkenFactor), 0), + Math.max((int) (color.getBlue() * darkenFactor), 0), + color.getAlpha() + ); } @NotNull @@ -824,10 +911,11 @@ public ProfileData findDataByPosition(double x, double y) { } private int findElementIndexByXY(double x, double y) { - if (x < 0 || x > maxRightOffset || y < 0) { + if (x < minLeftOffset || x > maxRightOffset || y < 0) { return -1; } - List objectsForThread = objects.getOrDefault(currentThreadId, Collections.emptyList()); + List objectsForThread = + objects.getOrDefault(currentThreadId, Collections.emptyList()); for (int i = 0; i < objectsForThread.size(); i++) { ProfileRectangle currentElement = objectsForThread.get(i); @@ -842,7 +930,8 @@ public void renderFoundItems(Finder.ThreadFindResult threadFindResult) { isSearchingInProgress = true; foundItems.clear(); - List objectsForThread = objects.getOrDefault(currentThreadId, Collections.emptyList()); + List objectsForThread = + objects.getOrDefault(currentThreadId, Collections.emptyList()); for (int i = 0; i < objectsForThread.size(); i++) { ProfileRectangle element = objectsForThread.get(i); @@ -856,14 +945,21 @@ public void renderFoundItems(Finder.ThreadFindResult threadFindResult) { currentFocusedFoundElement = 0; ProfileRectangle element = foundItems.get(currentFocusedFoundElement); - foundNavigationListener.onSelected(foundItems.size(), currentFocusedFoundElement, element.profileData); + foundNavigationListener.onSelected( + foundItems.size(), + currentFocusedFoundElement, + element.profileData + ); zoomAndPanDelegate.fitZoom(element, FIT_PADDING, ZoomAndPanDelegate.VerticalAlign.ENABLED); requestFocus(); } private void navigateToElement(Shape element) { - zoomAndPanDelegate.navigateToRectangle(element.getBounds2D(), ZoomAndPanDelegate.VerticalAlign.ENABLED); + zoomAndPanDelegate.navigateToRectangle( + element.getBounds2D(), + ZoomAndPanDelegate.VerticalAlign.ENABLED + ); } private void navigateToElement(Shape element, ZoomAndPanDelegate.VerticalAlign verticalAlign) { @@ -876,18 +972,18 @@ public boolean isSearchingInProgress() { private void addBookmark(ProfileRectangle foundElement, Color color, String title) { bookmarks.add( - new BookmarksRectangle( - title, - color, - foundElement.profileData.getThreadStartTimeInMillisecond(), - foundElement.profileData.getThreadEndTimeInMillisecond(), - foundElement.profileData.getGlobalStartTimeInMillisecond(), - foundElement.profileData.getGlobalEndTimeInMillisecond(), - foundElement.profileData.getLevel(), - currentThreadId, - maxBottomOffset, - isThreadTime - ) + new BookmarksRectangle( + title, + color, + foundElement.profileData.getThreadStartTimeInMillisecond(), + foundElement.profileData.getThreadEndTimeInMillisecond(), + foundElement.profileData.getGlobalStartTimeInMillisecond(), + foundElement.profileData.getGlobalEndTimeInMillisecond(), + foundElement.profileData.getLevel(), + currentThreadId, + maxBottomOffset, + isThreadTime + ) ); previewImageRepository.clear(); updatePreviewImage(); @@ -898,7 +994,8 @@ public void addBookmarkAtSelectedElement(BookMarkInfo bookMarkInfo) { return; } - List objectsForThread = objects.getOrDefault(currentThreadId, Collections.emptyList()); + List objectsForThread = + objects.getOrDefault(currentThreadId, Collections.emptyList()); ProfileRectangle selected = objectsForThread.get(currentSelectedElement); addBookmark(selected, bookMarkInfo.getColor(), bookMarkInfo.getTitle()); repaint(); @@ -945,7 +1042,8 @@ public ProfileData getSelected() { return null; } - List objectsForThread = objects.getOrDefault(currentThreadId, Collections.emptyList()); + List objectsForThread = + objects.getOrDefault(currentThreadId, Collections.emptyList()); return objectsForThread.get(currentSelectedElement).profileData; } @@ -969,7 +1067,11 @@ public void resetFoundItemToEnd() { private void focusFoundItem(int currentFocusedFoundElement) { ProfileRectangle found = foundItems.get(currentFocusedFoundElement); zoomAndPanDelegate.fitZoom(found, FIT_PADDING, ZoomAndPanDelegate.VerticalAlign.ENABLED); - foundNavigationListener.onSelected(foundItems.size(), currentFocusedFoundElement, found.profileData); + foundNavigationListener.onSelected( + foundItems.size(), + currentFocusedFoundElement, + found.profileData + ); } public void focusNextFoundItem() { @@ -1029,7 +1131,8 @@ public void centerSelectedElement() { if (currentSelectedElement < 0) { return; } - List objectsForThread = objects.getOrDefault(currentThreadId, Collections.emptyList()); + List objectsForThread = + objects.getOrDefault(currentThreadId, Collections.emptyList()); navigateToElement(objectsForThread.get(currentSelectedElement)); } @@ -1037,7 +1140,8 @@ public String copySelectedStacktrace() { if (currentSelectedElement < 0) { return null; } - List objectsForThread = objects.getOrDefault(currentThreadId, Collections.emptyList()); + List objectsForThread = + objects.getOrDefault(currentThreadId, Collections.emptyList()); ProfileRectangle selected = objectsForThread.get(currentSelectedElement); return createStackTrace(selected); } @@ -1056,7 +1160,11 @@ private String createStackTrace(ProfileRectangle selected) { public void resetZoom() { zoomAndPanDelegate.setTransform(new AffineTransform()); - zoomAndPanDelegate.fitZoom(new Rectangle.Double(0, 0, maxRightOffset, maxBottomOffset), 0, ZoomAndPanDelegate.VerticalAlign.NONE); + zoomAndPanDelegate.fitZoom( + new Rectangle.Double(minLeftOffset, 0, maxRightOffset - minLeftOffset, maxBottomOffset), + 0, + ZoomAndPanDelegate.VerticalAlign.NONE + ); repaint(); } @@ -1065,7 +1173,8 @@ public void fitSelectedElement() { if (foundItems.size() > 0) { rectangle = foundItems.get(currentFocusedFoundElement); } else if (currentSelectedElement >= 0) { - List objectsForThread = objects.getOrDefault(currentThreadId, Collections.emptyList()); + List objectsForThread = + objects.getOrDefault(currentThreadId, Collections.emptyList()); rectangle = objectsForThread.get(currentSelectedElement); } @@ -1073,7 +1182,11 @@ public void fitSelectedElement() { return; } - zoomAndPanDelegate.fitZoom(rectangle, FIT_PADDING, ZoomAndPanDelegate.VerticalAlign.ENABLED); + zoomAndPanDelegate.fitZoom( + rectangle, + FIT_PADDING, + ZoomAndPanDelegate.VerticalAlign.ENABLED + ); } public void scaleScreenToRange(double start, double end) { @@ -1129,7 +1242,8 @@ private int changeFontSize(int value) { */ public void selectProfileData(ProfileData selectedElement) { ProfileRectangle selectedRectangle = createProfileRectangle(selectedElement); - List objectsForThread = objects.getOrDefault(currentThreadId, Collections.emptyList()); + List objectsForThread = + objects.getOrDefault(currentThreadId, Collections.emptyList()); currentSelectedElement = objectsForThread.indexOf(selectedRectangle); if (currentSelectedElement < 0) { return; @@ -1154,7 +1268,8 @@ private ProfileRectangle getSelectedElement() { if (currentSelectedElement < 0) { return null; } - List objectsForThread = objects.getOrDefault(currentThreadId, Collections.emptyList()); + List objectsForThread = + objects.getOrDefault(currentThreadId, Collections.emptyList()); return objectsForThread.get(currentSelectedElement); } @@ -1194,16 +1309,21 @@ public void invalidateHighlighting() { } public class ProfilerPanelData { + public final List profileData; public final List markersData; - public ProfilerPanelData(List profileData, List markersData) { + public ProfilerPanelData( + List profileData, + List markersData + ) { this.profileData = profileData; this.markersData = markersData; } } public interface OnRightClickListener { + /** * Is called when right-clicked on bookmark. */ diff --git a/core/src/main/java/com/github/grishberg/profiler/chart/CalledStacktrace.kt b/core/src/main/java/com/github/grishberg/profiler/chart/CalledStacktrace.kt index 030cc13..041654b 100644 --- a/core/src/main/java/com/github/grishberg/profiler/chart/CalledStacktrace.kt +++ b/core/src/main/java/com/github/grishberg/profiler/chart/CalledStacktrace.kt @@ -39,7 +39,7 @@ class CalledStacktrace( addChildrenStrategy = findChildrenStrategy clearAllData() lastSelectedElement = profileData - renderer.currentThreadId = threadId + renderer.setCurrentThreadId(threadId) if (foundProfileData.isNotEmpty()) { dependenciesFoundAction?.onDependenciesFound(foundProfileData) @@ -68,7 +68,7 @@ class CalledStacktrace( if (!found.name.endsWith(".")) { return } - renderer.currentThreadId = threadId + renderer.setCurrentThreadId(threadId) lastSelectedElement = found //TODO: use coroutine @@ -111,7 +111,7 @@ class CalledStacktrace( ) { addChildrenStrategy = findDaggerCallerMethodStrategy clearAllData() - renderer.currentThreadId = threadId + renderer.setCurrentThreadId(threadId) lastSelectedElement = found lastDaggerFactory = null @@ -180,7 +180,7 @@ class CalledStacktrace( fun removeElements() { lastSelectedElement = null - renderer.currentThreadId = -1 + renderer.setCurrentThreadId(-1) clearAllData() } diff --git a/core/src/main/java/com/github/grishberg/profiler/chart/ElementsSelectionRenderer.kt b/core/src/main/java/com/github/grishberg/profiler/chart/ElementsSelectionRenderer.kt index 2500f60..7c0c075 100644 --- a/core/src/main/java/com/github/grishberg/profiler/chart/ElementsSelectionRenderer.kt +++ b/core/src/main/java/com/github/grishberg/profiler/chart/ElementsSelectionRenderer.kt @@ -7,7 +7,10 @@ import java.awt.Graphics2D import java.awt.geom.AffineTransform interface SelectionRenderer { - var currentThreadId: Int + val currentThreadId: Int + + fun setCurrentThreadId(id: Int) + fun draw( g: Graphics2D, at: AffineTransform, fm: FontMetrics, threadId: Int, selected: ProfileRectangle?, @@ -40,7 +43,13 @@ class ElementsSelectionRenderer( private val callTraceItems = mutableListOf() private var callerRectangles = mutableListOf() - override var currentThreadId = -1 + private var _currentThreadId = -1 + override val currentThreadId + get() = _currentThreadId + + override fun setCurrentThreadId(id: Int) { + _currentThreadId = id + } override fun draw( g: Graphics2D, at: AffineTransform, fm: FontMetrics, threadId: Int, @@ -131,7 +140,7 @@ class ElementsSelectionRenderer( } override fun clear() { - currentThreadId = -1 + _currentThreadId = -1 callTraceItems.clear() callerRectangles.clear() } diff --git a/core/src/main/java/com/github/grishberg/profiler/chart/Grid.kt b/core/src/main/java/com/github/grishberg/profiler/chart/Grid.kt index 9809964..3e5ed0c 100644 --- a/core/src/main/java/com/github/grishberg/profiler/chart/Grid.kt +++ b/core/src/main/java/com/github/grishberg/profiler/chart/Grid.kt @@ -7,7 +7,6 @@ import java.awt.FontMetrics import java.awt.Graphics2D import java.awt.geom.AffineTransform import java.awt.geom.Line2D -import java.util.ArrayList private const val AXISES_COUNT = 10 private const val MINIMUM_DURATION = 1.0 / 1000000000.0 @@ -21,6 +20,7 @@ class Grid( private val labelFont: Font, private val labelFontMetrics: FontMetrics ) { + private val labelColor = Color(246, 255, 241) private val lineColor = Color(191, 198, 187, 120) @@ -60,6 +60,8 @@ class Grid( return } val transformedScreenWidth = screenRight - screenLeft + //adjustScale(screenWidth.toDouble()) + while (transformedScreenWidth < minScreenRange) { if (distance / k < MINIMUM_DURATION) { return @@ -84,6 +86,7 @@ class Grid( while (distance > 0 && firstLineX < screenLeft && firstLineX - distance < screenLeft) { firstLineX += distance } + var x: Double = firstLineX var n = 0 diff --git a/core/src/main/java/com/github/grishberg/profiler/chart/preview/PreviewImageFactoryImpl.kt b/core/src/main/java/com/github/grishberg/profiler/chart/preview/PreviewImageFactoryImpl.kt index 1869d6f..5044ebd 100644 --- a/core/src/main/java/com/github/grishberg/profiler/chart/preview/PreviewImageFactoryImpl.kt +++ b/core/src/main/java/com/github/grishberg/profiler/chart/preview/PreviewImageFactoryImpl.kt @@ -18,16 +18,16 @@ class PreviewImageFactoryImpl( private val methodHeight = 2 override fun createPreview( - width: Int, - height: Int, + panelWidthPx: Int, + panelHeightPx: Int, result: AnalyzerResult, threadId: Int, previewType: PreviewType ): BufferedImage { - val image = BufferedImage(width, height, BufferedImage.TYPE_INT_RGB) + val image = BufferedImage(panelWidthPx, panelHeightPx, BufferedImage.TYPE_INT_RGB) val g = image.createGraphics() - drawMethods(result, threadId, previewType, g, width, height) + drawMethods(result, threadId, previewType, g, panelWidthPx, panelHeightPx) return image } @@ -41,13 +41,18 @@ class PreviewImageFactoryImpl( g.fillRect(0, 0, width, height) g.scale(1.0, 0.5) + val minLeft = when (previewType) { + PreviewType.PREVIEW_GLOBAL -> result.globalTimeBounds.getValue(threadId).minTime + PreviewType.PREVIEW_THREAD -> result.threadTimeBounds.getValue(threadId).minTime + PreviewType.THREAD_SWITCHER -> result.globalTimeBounds.getValue(result.mainThreadId).minTime + } val maxRight = when (previewType) { PreviewType.PREVIEW_GLOBAL -> result.globalTimeBounds.getValue(threadId).maxTime PreviewType.PREVIEW_THREAD -> result.threadTimeBounds.getValue(threadId).maxTime PreviewType.THREAD_SWITCHER -> result.globalTimeBounds.getValue(result.mainThreadId).maxTime } - val widthFactor: Double = maxRight / width + val widthFactor: Double = (maxRight - minLeft) / width val methods = result.data.getValue(threadId) @@ -71,7 +76,7 @@ class PreviewImageFactoryImpl( val w = (right - left) / widthFactor val h = bottom - top - val l = left / widthFactor + val l = (left - minLeft) / widthFactor g.fillRect(l.toInt(), top, w.toInt(), h) } @@ -93,7 +98,7 @@ class PreviewImageFactoryImpl( val w = (right - left) / widthFactor val h = result.maxLevel * methodHeight - val l = left / widthFactor + val l = (left - minLeft) / widthFactor g.color = bookmark.color g.fillRect(l.toInt(), 0, ceil(w).toInt(), h) } diff --git a/core/src/main/java/com/github/grishberg/profiler/comparator/aggregator/FlameChartAggregator.kt b/core/src/main/java/com/github/grishberg/profiler/comparator/aggregator/FlameChartAggregator.kt index 0112b9c..4a3efe3 100644 --- a/core/src/main/java/com/github/grishberg/profiler/comparator/aggregator/FlameChartAggregator.kt +++ b/core/src/main/java/com/github/grishberg/profiler/comparator/aggregator/FlameChartAggregator.kt @@ -6,6 +6,7 @@ import com.github.grishberg.profiler.comparator.findAllOf import com.github.grishberg.profiler.comparator.aggregator.model.AggregatedFlameProfileDataImpl import com.github.grishberg.profiler.comparator.aggregator.model.ComparableFlameChildHolder import com.github.grishberg.profiler.comparator.aggregator.model.FlameProfileData +import com.github.grishberg.profiler.core.ExtendedData import kotlin.math.min private const val INCLUDE_METHOD_THRESHOLD = 0.4 @@ -135,6 +136,7 @@ class FlameChartAggregator { override val threadEndTimeInMillisecond = Double.MAX_VALUE override val threadSelfTime = Double.MAX_VALUE override val threadStartTimeInMillisecond = Double.MIN_VALUE + override val extendedData: ExtendedData? = null } processChildrenToAggregate(listOf(fakeParent), fakeRoot) diff --git a/core/src/main/java/com/github/grishberg/profiler/core/AnalyzerResult.kt b/core/src/main/java/com/github/grishberg/profiler/core/AnalyzerResult.kt index 092ee08..f28fe62 100644 --- a/core/src/main/java/com/github/grishberg/profiler/core/AnalyzerResult.kt +++ b/core/src/main/java/com/github/grishberg/profiler/core/AnalyzerResult.kt @@ -8,4 +8,6 @@ interface AnalyzerResult { val threads: List val mainThreadId: Int val startTimeUs: Long // start recording time in System.upTimeInMs() + val minThreadTime: Double + val minGlobalTime: Double } diff --git a/core/src/main/java/com/github/grishberg/profiler/core/ExtendedData.kt b/core/src/main/java/com/github/grishberg/profiler/core/ExtendedData.kt new file mode 100644 index 0000000..8042ed1 --- /dev/null +++ b/core/src/main/java/com/github/grishberg/profiler/core/ExtendedData.kt @@ -0,0 +1,35 @@ +package com.github.grishberg.profiler.core + +sealed interface ExtendedData { + val tag: String? + val id:String + val fullName: String + data class CppFunctionData( + override val tag: String?, + override val id: String, + override val fullName: String, + val classOrNamespace: String, + val parameters:List, + val isUserCode: Boolean, + val fileName: String, + val vAddress: Long, + ):ExtendedData + + data class JavaMethodData(override val tag: String?, + override val id: String, + override val fullName: String, + val className: String, + val signature: String, + ):ExtendedData + + data class NoSymbolData(override val tag: String?, + override val id: String, + override val fullName: String, + val isKernel: Boolean, + ):ExtendedData + + data class SyscallData(override val tag: String?, + override val id: String, + override val fullName: String, + ):ExtendedData +} diff --git a/core/src/main/java/com/github/grishberg/profiler/core/ProfileData.kt b/core/src/main/java/com/github/grishberg/profiler/core/ProfileData.kt index 5a0b057..4043781 100644 --- a/core/src/main/java/com/github/grishberg/profiler/core/ProfileData.kt +++ b/core/src/main/java/com/github/grishberg/profiler/core/ProfileData.kt @@ -11,4 +11,5 @@ interface ProfileData { val globalSelfTime: Double val parent: ProfileData? val children: List + val extendedData: ExtendedData? } diff --git a/core/src/main/java/com/github/grishberg/profiler/ui/Main.java b/core/src/main/java/com/github/grishberg/profiler/ui/Main.java index 4507d2c..06f774f 100644 --- a/core/src/main/java/com/github/grishberg/profiler/ui/Main.java +++ b/core/src/main/java/com/github/grishberg/profiler/ui/Main.java @@ -1264,7 +1264,16 @@ protected void done() { systraceRecords); } pluginsFacade.setCurrentTraceProfiler(traceContainerResult); - ThreadItem firstThread = resultContainer.getResult().getThreads().get(0); + int mainThreadId = resultContainer.getResult().getMainThreadId(); + List threads = resultContainer.getResult().getThreads(); + ThreadItem firstThread = threads.get(0); + + for (int i = 0; i < threads.size(); i++) { + if (mainThreadId == threads.get(i).getThreadId()){ + firstThread = threads.get(i); + break; + } + } switchThreadsButton.switchThread(firstThread); pluginsFacade.setCurrentThread(firstThread); } catch (Exception e) { diff --git a/core/src/main/java/com/github/grishberg/profiler/ui/ZoomAndPanDelegate.java b/core/src/main/java/com/github/grishberg/profiler/ui/ZoomAndPanDelegate.java index 733d994..2abd628 100644 --- a/core/src/main/java/com/github/grishberg/profiler/ui/ZoomAndPanDelegate.java +++ b/core/src/main/java/com/github/grishberg/profiler/ui/ZoomAndPanDelegate.java @@ -39,6 +39,7 @@ public enum VerticalAlign { private final Rectangle visibleScreenBounds = new Rectangle(); private ScrollBoundsStrategy boundsStrategy; + private Point2D dataLeftTopCorner = new Point2D.Double(); private Point2D dataRightBottomCorner = new Point2D.Double(); public ZoomAndPanDelegate(Component targetComponent, int topOffset, ScrollBoundsStrategy boundsStrategy) { @@ -50,10 +51,12 @@ public ZoomAndPanDelegate(Component targetComponent, int topOffset, ScrollBounds targetComponent.addMouseWheelListener(this); } - public void updateRightBottomCorner(double maxX, double maxY) { + public void updateBounds(double minX, double maxX, double maxY) { dataRightBottomCorner.setLocation(maxX, maxY); + dataLeftTopCorner.setLocation(minX, 0.0); } + public void setMouseEventsListener(MouseEventsListener l) { mouseEventsListener = l; } diff --git a/core/src/test/kotlin/com/github/grishberg/profiler/analyzer/SimplePerfAnalyzerTest.kt b/core/src/test/kotlin/com/github/grishberg/profiler/analyzer/SimplePerfAnalyzerTest.kt new file mode 100644 index 0000000..e0a812e --- /dev/null +++ b/core/src/test/kotlin/com/github/grishberg/profiler/analyzer/SimplePerfAnalyzerTest.kt @@ -0,0 +1,36 @@ +package com.github.grishberg.profiler.analyzer + +import com.github.grishberg.profiler.analyzer.converter.NoOpNameConverter +import com.github.grishberg.profiler.common.AppLogger +import com.github.grishberg.profiler.common.TestLogger +import java.io.File +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertNotNull +import org.junit.Test + +internal class SimplePerfAnalyzerTest { + + private val logger: AppLogger = TestLogger() + private val underTest = SimplePerfAnalyzer(logger, NoOpNameConverter) + + @Test + fun `test simpleperf analyzer`() { + val testFile = getSimplePerfFile() + val result = underTest.analyze(testFile) + + assertEquals(24, result.threads.size) + + val mainThreadData = result.data[result.mainThreadId] + + assertNotNull(mainThreadData) + + assertEquals(38319, mainThreadData!!.size) + + } + + private fun getSimplePerfFile(): File { + val classLoader = javaClass.classLoader + val filePath = classLoader.getResource("simpleperf.trace")?.file ?: throw IllegalStateException() + return File(filePath) + } +} diff --git a/core/src/test/kotlin/com/github/grishberg/profiler/analyzer/TraceAnalyzerTest.kt b/core/src/test/kotlin/com/github/grishberg/profiler/analyzer/TraceAnalyzerTest.kt new file mode 100644 index 0000000..1fb5fe8 --- /dev/null +++ b/core/src/test/kotlin/com/github/grishberg/profiler/analyzer/TraceAnalyzerTest.kt @@ -0,0 +1,21 @@ +package com.github.grishberg.profiler.analyzer + +import com.github.grishberg.profiler.common.TestLogger +import java.io.File +import org.junit.Test + +internal class TraceAnalyzerTest { + private val underTest = TraceAnalyzer(TestLogger()) + + @Test + fun `test simplePerf trace parsing`(){ + underTest.analyze(getSimplePerfFile()) + } + + private fun getSimplePerfFile(): File { + val classLoader = javaClass.classLoader + val filePath = classLoader.getResource("simpleperf.trace")?.file ?: throw IllegalStateException() + return File(filePath) + } + +} diff --git a/core/src/test/kotlin/com/github/grishberg/profiler/common/TestLogger.kt b/core/src/test/kotlin/com/github/grishberg/profiler/common/TestLogger.kt new file mode 100644 index 0000000..cc8a0bc --- /dev/null +++ b/core/src/test/kotlin/com/github/grishberg/profiler/common/TestLogger.kt @@ -0,0 +1,24 @@ +package com.github.grishberg.profiler.common + +class TestLogger: AppLogger { + + override fun d(msg: String) { + println(msg) + } + + override fun e(msg: String) { + println(msg) + } + + override fun e(msg: String, t: Throwable) { + println(msg) + } + + override fun w(msg: String) { + println(msg) + } + + override fun i(msg: String) { + println(msg) + } +} diff --git a/core/src/test/resources/simpleperf.trace b/core/src/test/resources/simpleperf.trace new file mode 100644 index 0000000..ce0901d Binary files /dev/null and b/core/src/test/resources/simpleperf.trace differ diff --git a/gradle.properties b/gradle.properties index 5aec92b..926d556 100644 --- a/gradle.properties +++ b/gradle.properties @@ -8,7 +8,7 @@ studioCompilePath=/Applications/Android Studio.app/Contents pluginGroup = com.github.grishberg pluginName = android-methods-profiler -yampVersion = 24.07.31 +yampVersion = 24.08.22 # See https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html # for insight into build numbers and IntelliJ Platform versions.