From f84210324a674847c1b23b1f4202159b7851ee0f Mon Sep 17 00:00:00 2001 From: Oleg Baskakov Date: Fri, 5 Nov 2021 13:19:15 -0700 Subject: [PATCH 1/2] Create a config tab to show user trace requests add global TracerUserConfig to store raw user requests changed TraceTarget.Method to duplicate traceOption state since this information isn't available in MethodFqMatcher created a unit test for a UI component increased -XX:ReservedCodeCacheSize --- build.gradle.kts | 2 +- .../google/idea/perf/tracer/TracerCommand.kt | 8 +- .../idea/perf/tracer/TracerController.kt | 27 +++--- .../idea/perf/tracer/TracerUserConfig.kt | 61 ++++++++++++ .../idea/perf/tracer/ui/TracerConfigTab.kt | 55 +++++++++++ .../google/idea/perf/tracer/ui/TracerPanel.kt | 19 +++- .../perf/tracer/ui/TracerConfigTabTest.kt | 96 +++++++++++++++++++ 7 files changed, 250 insertions(+), 18 deletions(-) create mode 100644 src/main/java/com/google/idea/perf/tracer/TracerUserConfig.kt create mode 100644 src/main/java/com/google/idea/perf/tracer/ui/TracerConfigTab.kt create mode 100644 src/test/java/com/google/idea/perf/tracer/ui/TracerConfigTabTest.kt diff --git a/build.gradle.kts b/build.gradle.kts index 3cdad86..fa1a9a1 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -129,7 +129,7 @@ tasks.runIde { jvmArgs("-ea") // Copy over some JVM args from IntelliJ. - jvmArgs("-XX:ReservedCodeCacheSize=240m") + jvmArgs("-XX:ReservedCodeCacheSize=512m") jvmArgs("-XX:+UseConcMarkSweepGC") jvmArgs("-XX:SoftRefLRUPolicyMSPerMB=50") jvmArgs("-XX:CICompilerCount=2") diff --git a/src/main/java/com/google/idea/perf/tracer/TracerCommand.kt b/src/main/java/com/google/idea/perf/tracer/TracerCommand.kt index a5b6c17..e0caeb6 100644 --- a/src/main/java/com/google/idea/perf/tracer/TracerCommand.kt +++ b/src/main/java/com/google/idea/perf/tracer/TracerCommand.kt @@ -18,6 +18,7 @@ package com.google.idea.perf.tracer import com.google.idea.perf.tracer.TraceOption.COUNT_AND_WALL_TIME import com.google.idea.perf.tracer.TraceOption.COUNT_ONLY +import com.google.idea.perf.tracer.TraceOption.UNTRACE /** A tracer CLI command */ sealed class TracerCommand { @@ -56,7 +57,8 @@ sealed class TracerCommand { /** Represents what to trace */ enum class TraceOption { COUNT_AND_WALL_TIME, - COUNT_ONLY; + COUNT_ONLY, + UNTRACE; } /** A set of methods that the tracer will trace. */ @@ -68,7 +70,9 @@ sealed class TraceTarget { data class Method( val className: String, val methodName: String?, - val parameterIndexes: List? = emptyList() + val parameterIndexes: List? = emptyList(), + // a redundant option to support user config tab + var traceOption: TraceOption = COUNT_AND_WALL_TIME ): TraceTarget() val errors: List diff --git a/src/main/java/com/google/idea/perf/tracer/TracerController.kt b/src/main/java/com/google/idea/perf/tracer/TracerController.kt index 66fa674..d851e0e 100644 --- a/src/main/java/com/google/idea/perf/tracer/TracerController.kt +++ b/src/main/java/com/google/idea/perf/tracer/TracerController.kt @@ -86,13 +86,12 @@ class TracerController( // Special case: handle this command while we're still on the EDT. val path = cmd.substringAfter("save").trim() savePngFromEdt(path) - } - else { + } else { executor.execute { handleCommand(cmd) } } } - private fun handleCommand(commandString: String) { + fun handleCommand(commandString: String) { val command = parseMethodTracerCommand(commandString) val errors = command.errors @@ -100,7 +99,6 @@ class TracerController( displayWarning(errors.joinToString("\n")) return } - handleCommand(command) } @@ -109,7 +107,9 @@ class TracerController( is TracerCommand.Clear -> { CallTreeManager.clearCallTrees() } + is TracerCommand.Reset -> { + TracerUserConfig.resetAll() runWithProgress { progress -> val oldRequests = TracerConfig.clearAllRequests() val affectedClasses = TracerConfigUtil.getAffectedClasses(oldRequests) @@ -117,9 +117,9 @@ class TracerController( CallTreeManager.clearCallTrees() } } + is TracerCommand.Trace -> { val countOnly = command.traceOption == TraceOption.COUNT_ONLY - when (command.target) { is TraceTarget.All -> { when { @@ -127,7 +127,14 @@ class TracerController( else -> handleCommand(TracerCommand.Reset) } } + is TraceTarget.Method -> { + if (command.enable) { + TracerUserConfig.addUserTraceRequest(command.target) + } else { + command.target.traceOption = TraceOption.UNTRACE + TracerUserConfig.addUserUntraceRequest(command.target) + } runWithProgress { progress -> val clazz = command.target.className val method = command.target.methodName ?: "*" @@ -145,6 +152,7 @@ class TracerController( } } } + else -> { displayWarning("Command not implemented") } @@ -165,11 +173,9 @@ class TracerController( progress.checkCanceled() try { instrumentation.retransformClasses(clazz) - } - catch (e: UnmodifiableClassException) { + } catch (e: UnmodifiableClassException) { LOG.info("Cannot instrument non-modifiable class: ${clazz.name}") - } - catch (e: Throwable) { + } catch (e: Throwable) { LOG.error("Failed to retransform class: ${clazz.name}", e) } if (!progress.isIndeterminate) { @@ -195,8 +201,7 @@ class TracerController( getApplication().executeOnPooledThread { try { ImageIO.write(img, "png", file) - } - catch (e: IOException) { + } catch (e: IOException) { displayWarning("Failed to write png to $path", e) } } diff --git a/src/main/java/com/google/idea/perf/tracer/TracerUserConfig.kt b/src/main/java/com/google/idea/perf/tracer/TracerUserConfig.kt new file mode 100644 index 0000000..2a05d0d --- /dev/null +++ b/src/main/java/com/google/idea/perf/tracer/TracerUserConfig.kt @@ -0,0 +1,61 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.idea.perf.tracer + +import java.util.* + +/** + * [TracerUserConfig] keeps track of which methods should be traced and `untrace`d. + * When a user `untrace`s some request, it will be removed if it was created before. + */ +object TracerUserConfig { + + private val userTraceRequests = Collections.synchronizedMap(LinkedHashMap()) + + fun cloneUserTraceRequests(): List { + return userTraceRequests.values.toList() + } + + fun addUserTraceRequest(entry: TraceTarget.Method) { + val plainTextKey = concatClassAndMethod(entry) + userTraceRequests[plainTextKey] = entry + } + + @Synchronized + fun addUserUntraceRequest(entry: TraceTarget.Method) { + val classAndMethod = concatClassAndMethod(entry) + val value = userTraceRequests[classAndMethod] + if (value != null && value.traceOption != TraceOption.UNTRACE) { + userTraceRequests.remove(classAndMethod) + } else { + userTraceRequests[classAndMethod] = entry + } + } + + @Synchronized + fun resetAll() { + val keys = userTraceRequests.keys.toList() + for (key in keys) { + userTraceRequests.remove(key) + } + } + + private fun concatClassAndMethod(entry: TraceTarget.Method): String { + return "${entry.className}#${entry.methodName ?: ""}" + } + +} diff --git a/src/main/java/com/google/idea/perf/tracer/ui/TracerConfigTab.kt b/src/main/java/com/google/idea/perf/tracer/ui/TracerConfigTab.kt new file mode 100644 index 0000000..b36daa4 --- /dev/null +++ b/src/main/java/com/google/idea/perf/tracer/ui/TracerConfigTab.kt @@ -0,0 +1,55 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.idea.perf.tracer.ui + +import com.google.idea.perf.tracer.TraceOption +import com.google.idea.perf.tracer.TraceTarget +import com.intellij.ui.components.JBTextArea + +/** Displays a list of trace/untrace commands as plain text. */ +class TracerConfigTab : JBTextArea() { + + private var previousCommandsList: List = emptyList() + + fun setTracingConfig(newStats: List) { + if (previousCommandsList == newStats) { + return + } + previousCommandsList = newStats + document.remove(0, document.length) + val tmp = newStats.joinToString( + separator = "\n", + transform = TracerConfigTab::methodToString + ) + append(tmp) + } + + companion object { + private fun methodToString(method: TraceTarget.Method): String { + val option = when (method.traceOption) { + TraceOption.COUNT_AND_WALL_TIME -> "trace" + TraceOption.COUNT_ONLY -> "trace count" + TraceOption.UNTRACE -> "untrace" + } + if (method.methodName == "*") { + return "$option ${method.className}" + } else { + return "$option ${method.className}::${method.methodName}" + } + } + } +} diff --git a/src/main/java/com/google/idea/perf/tracer/ui/TracerPanel.kt b/src/main/java/com/google/idea/perf/tracer/ui/TracerPanel.kt index ae9b27a..8fb1684 100644 --- a/src/main/java/com/google/idea/perf/tracer/ui/TracerPanel.kt +++ b/src/main/java/com/google/idea/perf/tracer/ui/TracerPanel.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 Google LLC + * Copyright 2021 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,7 +19,9 @@ package com.google.idea.perf.tracer.ui import com.google.idea.perf.tracer.CallTreeManager import com.google.idea.perf.tracer.CallTreeUtil import com.google.idea.perf.tracer.TracerController +import com.google.idea.perf.tracer.TracerUserConfig import com.google.idea.perf.util.formatNsInMs +import com.google.idea.perf.util.onDispose import com.intellij.CommonBundle import com.intellij.ide.BrowserUtil import com.intellij.openapi.Disposable @@ -29,7 +31,6 @@ import com.intellij.openapi.editor.ex.util.EditorUtil import com.intellij.openapi.progress.ProgressIndicator import com.intellij.openapi.progress.util.ProgressIndicatorBase import com.intellij.openapi.project.Project -import com.google.idea.perf.util.onDispose import com.intellij.openapi.ui.ComboBox import com.intellij.openapi.ui.MessageType import com.intellij.openapi.ui.popup.util.PopupUtil @@ -64,7 +65,8 @@ import javax.swing.SwingConstants.HORIZONTAL /** * This is the main tracer panel containing the command line, call tree view, - * overhead labels, etc. It also polls for new call tree data in [updateCallTree]. + * overhead labels, a config tab with tracing commands etc. + * It also polls for new call tree data in [updateCallTree]. * * This panel is displayed via the [TracerDialog]. */ @@ -78,6 +80,7 @@ class TracerPanel( private var showingEdtOnly = false private val listView: TracerTable private val treeView: TracerTree + internal val configView: TracerConfigTab private val tracingOverheadLabel: JBLabel private val uiOverheadLabel: JBLabel private var uiOverhead = 0L @@ -158,6 +161,13 @@ class TracerPanel( .setSideComponent(createTabSideComponent()) tabs.addTab(treeTab) + // Config view. + configView = TracerConfigTab() + val configTab = TabInfo(JBScrollPane(configView)) + .setText("Config") + .setSideComponent(createTabSideComponent()) + tabs.addTab(configTab) + // Tracing overhead label. val overheadFont = JBFont .create(EditorUtil.getEditorFont()) @@ -217,7 +227,7 @@ class TracerPanel( } } - private fun updateCallTree() { + fun updateCallTree() { // In order to measure tracer UI overhead we need to measure the time it // takes to update the tree model *and* the time it takes to run all the // invokeLater tasks generated by the update (but, exclude the time for @@ -239,6 +249,7 @@ class TracerPanel( val stats = CallTreeUtil.computeFlatTracepointStats(treeSnapshot) listView.setTracepointStats(stats) treeView.setCallTree(treeSnapshot) + configView.setTracingConfig(TracerUserConfig.cloneUserTraceRequests()) // Estimate tracing overhead. val tracingOverhead = CallTreeUtil.estimateTracingOverhead(treeSnapshot) diff --git a/src/test/java/com/google/idea/perf/tracer/ui/TracerConfigTabTest.kt b/src/test/java/com/google/idea/perf/tracer/ui/TracerConfigTabTest.kt new file mode 100644 index 0000000..a45cad5 --- /dev/null +++ b/src/test/java/com/google/idea/perf/tracer/ui/TracerConfigTabTest.kt @@ -0,0 +1,96 @@ +/* + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.idea.perf.tracer.ui + +import com.google.idea.perf.sample.Sample +import com.google.idea.perf.tracer.TracerController +import com.intellij.openapi.application.invokeAndWaitIfNeeded +import com.intellij.testFramework.fixtures.BasePlatformTestCase +import junit.framework.TestCase +import org.junit.Test +import kotlin.reflect.KFunction +import kotlin.reflect.jvm.javaMethod + +internal class TracerConfigTabTest : BasePlatformTestCase() { + + private lateinit var tracer: TracerController + private lateinit var tracerPanel: TracerPanel + + override fun setUp() { + super.setUp() + tracerPanel = invokeAndWaitIfNeeded { TracerPanel(project, testRootDisposable) } + tracer = tracerPanel.controller + } + + override fun tearDown() { + try { + tracer.handleCommandFromTest("reset") + } catch (e: Throwable) { + addSuppressedException(e) + } finally { + super.tearDown() + } + } + + // Avoid deadlock when waiting on the tracer. + override fun runInDispatchThread(): Boolean = false + + @Test + fun testSimpleTracesConfig() { + for (method in listOf(Sample::a, Sample::c, Sample::b, Sample::e)) { + tracer.handleCommand("trace ${format(method)}") + } + tracerPanel.updateCallTree() + TestCase.assertEquals( + """ + trace com.google.idea.perf.sample.Sample::a + trace com.google.idea.perf.sample.Sample::c + trace com.google.idea.perf.sample.Sample::b + trace com.google.idea.perf.sample.Sample::e + """.trimIndent(), + tracerPanel.configView.text + ) + for (method in listOf(Sample::b, Sample::c, Sample::d)) { + tracer.handleCommand("untrace ${format(method)}") + } + tracerPanel.updateCallTree() + TestCase.assertEquals( + """ + trace com.google.idea.perf.sample.Sample::a + trace com.google.idea.perf.sample.Sample::e + untrace com.google.idea.perf.sample.Sample::d + """.trimIndent(), + tracerPanel.configView.text + ) + + tracer.handleCommand("reset") + tracerPanel.updateCallTree() + TestCase.assertEquals( + """ + """.trimIndent(), + tracerPanel.configView.text + ) + + } + + private fun format(method: KFunction<*>): String { + val className = method.javaMethod!!.declaringClass.name + val methodName = method.name + return "$className#$methodName" + } + +} From 13c74c1c9eb4da51e72d3a47a5ca939e3bbad7c0 Mon Sep 17 00:00:00 2001 From: Oleg Baskakov Date: Fri, 5 Nov 2021 16:29:36 -0700 Subject: [PATCH 2/2] add support to load configs and stacktraces --- docs/user-guide.md | 6 ++ .../perf/tracer/TracerCompletionProvider.kt | 4 +- .../idea/perf/tracer/TracerController.kt | 69 ++++++++++++++++++- .../idea/perf/tracer/TracerUserConfig.kt | 13 ++-- 4 files changed, 82 insertions(+), 10 deletions(-) diff --git a/docs/user-guide.md b/docs/user-guide.md index 305f959..9f7d7a7 100644 --- a/docs/user-guide.md +++ b/docs/user-guide.md @@ -75,6 +75,12 @@ slightly more experimental than the main method tracer. +Config and stacktrace loaders +--- +There are two new commands which could be used from a `Config` tab or with an absolute file path: +* `load` loads commands such as `trace`/`untrace` in the same format as the tracer +* `stacktrace` loads functions from a given stacktrace in plain text form + FAQ --- diff --git a/src/main/java/com/google/idea/perf/tracer/TracerCompletionProvider.kt b/src/main/java/com/google/idea/perf/tracer/TracerCompletionProvider.kt index 3e39483..205b0c3 100644 --- a/src/main/java/com/google/idea/perf/tracer/TracerCompletionProvider.kt +++ b/src/main/java/com/google/idea/perf/tracer/TracerCompletionProvider.kt @@ -66,12 +66,14 @@ class TracerCompletionProvider : TextCompletionProvider, DumbAware { when (tokenIndex) { 0 -> { // We want all commands to be shown regardless of the prefix (for discoverability). - val allCommands = setOf("clear", "reset", "trace", "untrace") + val allCommands = setOf("clear", "load", "reset", "stacktrace", "trace", "untrace") val prefixMatcher = LenientPrefixMatcher(result.prefixMatcher, allCommands) result = result.withPrefixMatcher(prefixMatcher) result.addElement(LookupElementBuilder.create("clear")) + result.addElement(LookupElementBuilder.create("load")) result.addElement(LookupElementBuilder.create("reset")) + result.addElement(LookupElementBuilder.create("stacktrace")) result.addElement( LookupElementBuilder.create("trace") .withTailText(" ") diff --git a/src/main/java/com/google/idea/perf/tracer/TracerController.kt b/src/main/java/com/google/idea/perf/tracer/TracerController.kt index d851e0e..4a44a8e 100644 --- a/src/main/java/com/google/idea/perf/tracer/TracerController.kt +++ b/src/main/java/com/google/idea/perf/tracer/TracerController.kt @@ -86,12 +86,69 @@ class TracerController( // Special case: handle this command while we're still on the EDT. val path = cmd.substringAfter("save").trim() savePngFromEdt(path) + } else if (cmd.startsWith("load")) { + val path = cmd.substringAfter("load").trim() + // TODO(baskakov): handle file not found error + executor.execute { handleLoadConfig(path) } + } else if (cmd.startsWith("stacktrace")) { + val path = cmd.substringAfter("stacktrace").trim() + // TODO(baskakov): handle file not found error + executor.execute { handleLoadStacktrace(path) } } else { executor.execute { handleCommand(cmd) } } } - fun handleCommand(commandString: String) { + private fun handleLoadConfig(path: String) { + val lines = linesFromPathOrConfig(path) + if (lines.isEmpty()) { + displayWarning("config is empty") + return + } + handleCommand("reset") + lines.forEach { + displayInfo("processing: $it") + handleCommand(it) + } + } + + private fun linesFromPathOrConfig(path: String): List { + return if (path.isBlank()) { + view.configView.text.split("\n") + } else { + val file = File(path) + if (!file.isFile) { + displayWarning("File not found: $path") + return emptyList() + } + file.readLines() + } + .filter { it.isNotBlank() } + } + + private fun handleLoadStacktrace(path: String) { + val lines = linesFromPathOrConfig(path) + if (lines.isEmpty()) { + displayWarning("stacktrace is empty") + return + } + handleCommand("reset") + for (line in lines) { + val oneTraceLine = line.trim().substringAfter("at ", "") + if (oneTraceLine.isBlank()) continue + val classAndMethod = oneTraceLine + .substringAfter("/") + .substringBefore("(") + val fqClassName = classAndMethod.substringBeforeLast(".") + val functionName = classAndMethod.substringAfterLast(".") + // TODO(baskakov): add unit test for: + // at org.jetbrains.kotlin.types.StarProjectionImpl$_type$2.invoke(StarProjectionImpl.kt:35) + val commandString = "trace ${fqClassName}#${functionName}" + handleCommand(commandString) + } + } + + private fun handleCommand(commandString: String) { val command = parseMethodTracerCommand(commandString) val errors = command.errors @@ -145,7 +202,8 @@ class TracerController( tracedParams = command.target.parameterIndexes!! ) val request = TracerConfigUtil.appendTraceRequest(methodPattern, config) - val affectedClasses = TracerConfigUtil.getAffectedClasses(listOf(request)) + val affectedClasses = + TracerConfigUtil.getAffectedClasses(listOf(request)) retransformClasses(affectedClasses, progress) CallTreeManager.clearCallTrees() } @@ -214,6 +272,13 @@ class TracerController( } } + private fun displayInfo(infoString: String, e: Throwable? = null) { + LOG.info(infoString, e) + invokeLater { + view.showCommandLinePopup(infoString, MessageType.INFO) + } + } + private fun runWithProgress(action: (ProgressIndicator) -> T): T { val indicator = view.createProgressIndicator() val computable = Computable { action(indicator) } diff --git a/src/main/java/com/google/idea/perf/tracer/TracerUserConfig.kt b/src/main/java/com/google/idea/perf/tracer/TracerUserConfig.kt index 2a05d0d..c48d91f 100644 --- a/src/main/java/com/google/idea/perf/tracer/TracerUserConfig.kt +++ b/src/main/java/com/google/idea/perf/tracer/TracerUserConfig.kt @@ -24,12 +24,14 @@ import java.util.* */ object TracerUserConfig { - private val userTraceRequests = Collections.synchronizedMap(LinkedHashMap()) + private val userTraceRequests = LinkedHashMap() + @Synchronized fun cloneUserTraceRequests(): List { return userTraceRequests.values.toList() } + @Synchronized fun addUserTraceRequest(entry: TraceTarget.Method) { val plainTextKey = concatClassAndMethod(entry) userTraceRequests[plainTextKey] = entry @@ -38,8 +40,8 @@ object TracerUserConfig { @Synchronized fun addUserUntraceRequest(entry: TraceTarget.Method) { val classAndMethod = concatClassAndMethod(entry) - val value = userTraceRequests[classAndMethod] - if (value != null && value.traceOption != TraceOption.UNTRACE) { + val oldValue = userTraceRequests[classAndMethod] + if (oldValue != null && oldValue.traceOption != TraceOption.UNTRACE) { userTraceRequests.remove(classAndMethod) } else { userTraceRequests[classAndMethod] = entry @@ -48,10 +50,7 @@ object TracerUserConfig { @Synchronized fun resetAll() { - val keys = userTraceRequests.keys.toList() - for (key in keys) { - userTraceRequests.remove(key) - } + userTraceRequests.clear() } private fun concatClassAndMethod(entry: TraceTarget.Method): String {