From 37d58601b9a3d125a2eb88a1aedf237850f8ba74 Mon Sep 17 00:00:00 2001 From: Yuriy Artamonov Date: Thu, 17 Oct 2024 10:43:52 +0200 Subject: [PATCH] [performance] IJPL-161370 Detect UI freezes caused by third-party plugins and disable them GitOrigin-RevId: 57013d91d5767602b1cb6db72d644e83bed2339b --- .../intellij.platform.diagnostic.freezes.iml | 1 + .../intellij.platform.diagnostic.freezes.xml | 5 ++ .../messages/PluginFreezeBundle.properties | 2 +- .../plugin/freeze/PluginFreezeNotifier.kt | 19 +++--- .../plugin/freeze/PluginFreezeWatcher.kt | 66 ++++++++++++++++--- .../freeze/PluginsFreezesServiceState.kt | 21 +++--- .../ResetFreezeNotificationStateAction.kt | 14 ++++ .../intellij/diagnostic/dropErrorActions.kt | 27 -------- .../LifecycleUsageTriggerCollector.java | 25 ++++++- .../src/idea/PlatformActions.xml | 1 - .../core/resources/META-INF/plugin.xml | 9 ++- .../actions/SimulateFreeze.kt | 32 +++++++++ 12 files changed, 162 insertions(+), 60 deletions(-) create mode 100644 platform/diagnostic/freezeInPluginNotifier/src/com/intellij/platform/diagnostic/plugin/freeze/ResetFreezeNotificationStateAction.kt create mode 100644 plugins/performanceTesting/core/src/com/jetbrains/performancePlugin/actions/SimulateFreeze.kt diff --git a/platform/diagnostic/freezeInPluginNotifier/intellij.platform.diagnostic.freezes.iml b/platform/diagnostic/freezeInPluginNotifier/intellij.platform.diagnostic.freezes.iml index e149dc717791..924234c1e094 100644 --- a/platform/diagnostic/freezeInPluginNotifier/intellij.platform.diagnostic.freezes.iml +++ b/platform/diagnostic/freezeInPluginNotifier/intellij.platform.diagnostic.freezes.iml @@ -17,5 +17,6 @@ + \ No newline at end of file diff --git a/platform/diagnostic/freezeInPluginNotifier/resources/intellij.platform.diagnostic.freezes.xml b/platform/diagnostic/freezeInPluginNotifier/resources/intellij.platform.diagnostic.freezes.xml index 8658cdd38ae9..abec1cf61494 100644 --- a/platform/diagnostic/freezeInPluginNotifier/resources/intellij.platform.diagnostic.freezes.xml +++ b/platform/diagnostic/freezeInPluginNotifier/resources/intellij.platform.diagnostic.freezes.xml @@ -6,4 +6,9 @@ + + + + \ No newline at end of file diff --git a/platform/diagnostic/freezeInPluginNotifier/resources/messages/PluginFreezeBundle.properties b/platform/diagnostic/freezeInPluginNotifier/resources/messages/PluginFreezeBundle.properties index fb09c32872a9..d3a3ff1f6567 100644 --- a/platform/diagnostic/freezeInPluginNotifier/resources/messages/PluginFreezeBundle.properties +++ b/platform/diagnostic/freezeInPluginNotifier/resources/messages/PluginFreezeBundle.properties @@ -1,4 +1,4 @@ action.disable.plugin.text=Disable plugin action.close.panel.text=Dismiss action.ignore.plugin.text=Ignore freezes in plugin -notification.content.plugin.caused.freeze.detected=Freeze caused by the {0} plugin \ No newline at end of file +notification.content.plugin.caused.freeze.detected=Plugin ''{0}'' might be slowing things down \ No newline at end of file diff --git a/platform/diagnostic/freezeInPluginNotifier/src/com/intellij/platform/diagnostic/plugin/freeze/PluginFreezeNotifier.kt b/platform/diagnostic/freezeInPluginNotifier/src/com/intellij/platform/diagnostic/plugin/freeze/PluginFreezeNotifier.kt index de3da499996c..f1a4467b8505 100644 --- a/platform/diagnostic/freezeInPluginNotifier/src/com/intellij/platform/diagnostic/plugin/freeze/PluginFreezeNotifier.kt +++ b/platform/diagnostic/freezeInPluginNotifier/src/com/intellij/platform/diagnostic/plugin/freeze/PluginFreezeNotifier.kt @@ -1,12 +1,12 @@ // Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package com.intellij.platform.diagnostic.plugin.freeze +import com.intellij.featureStatistics.fusCollectors.LifecycleUsageTriggerCollector import com.intellij.ide.plugins.PluginManagerCore import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.application.impl.ApplicationInfoImpl import com.intellij.openapi.extensions.PluginId import com.intellij.openapi.fileEditor.FileEditor -import com.intellij.openapi.fileEditor.FileEditorManager import com.intellij.openapi.project.Project import com.intellij.openapi.vfs.VirtualFile import com.intellij.ui.EditorNotificationPanel @@ -21,23 +21,25 @@ internal class PluginFreezeNotifier : EditorNotificationProvider { private val freezeStorageService = PluginsFreezesService.getInstance() override fun collectNotificationData(project: Project, file: VirtualFile): Function? { - val frozenPlugin = freezeWatcher.latestFrozenPlugin ?: return null + val frozenPlugin = freezeWatcher.getFreezeReason() ?: return null val pluginDescriptor = PluginManagerCore.getPlugin(frozenPlugin) ?: return null val application = ApplicationManager.getApplication() if (pluginDescriptor.isImplementationDetail || ApplicationInfoImpl.getShadowInstance().isEssentialPlugin(frozenPlugin)) return null if (pluginDescriptor.isBundled && !application.isEAP && !application.isInternal) return null - if (freezeStorageService.shouldBeIgnored(frozenPlugin)) return null - - freezeStorageService.setLatestFreezeDate(frozenPlugin) return Function { EditorNotificationPanel(EditorNotificationPanel.Status.Warning).apply { text = PluginFreezeBundle.message("notification.content.plugin.caused.freeze.detected", pluginDescriptor.name) createActionLabel(PluginFreezeBundle.message("action.disable.plugin.text")) { disablePlugin(frozenPlugin) + + LifecycleUsageTriggerCollector.pluginDisabledOnFreeze(frozenPlugin) } createActionLabel(PluginFreezeBundle.message("action.ignore.plugin.text")) { freezeStorageService.mutePlugin(frozenPlugin) + + LifecycleUsageTriggerCollector.pluginFreezeIgnored(frozenPlugin) + closePanel(project) } createActionLabel(PluginFreezeBundle.message("action.close.panel.text")) { @@ -47,7 +49,7 @@ internal class PluginFreezeNotifier : EditorNotificationProvider { } } - //TODO support dynamic plugins + // TODO support dynamic plugins private fun disablePlugin(frozenPlugin: PluginId) { val pluginDisabled = PluginManagerCore.disablePlugin(frozenPlugin) if (pluginDisabled) { @@ -56,8 +58,7 @@ internal class PluginFreezeNotifier : EditorNotificationProvider { } private fun closePanel(project: Project) { - freezeWatcher.latestFrozenPlugin = null - FileEditorManager.getInstance(project).openFiles.forEach { it -> EditorNotifications.getInstance(project).updateNotifications(it) } + freezeWatcher.reset() + EditorNotifications.getInstance(project).updateAllNotifications() } - } \ No newline at end of file diff --git a/platform/diagnostic/freezeInPluginNotifier/src/com/intellij/platform/diagnostic/plugin/freeze/PluginFreezeWatcher.kt b/platform/diagnostic/freezeInPluginNotifier/src/com/intellij/platform/diagnostic/plugin/freeze/PluginFreezeWatcher.kt index 9db9eff4d6e5..07af9d6c9ccf 100644 --- a/platform/diagnostic/freezeInPluginNotifier/src/com/intellij/platform/diagnostic/plugin/freeze/PluginFreezeWatcher.kt +++ b/platform/diagnostic/freezeInPluginNotifier/src/com/intellij/platform/diagnostic/plugin/freeze/PluginFreezeWatcher.kt @@ -2,24 +2,30 @@ package com.intellij.platform.diagnostic.plugin.freeze import com.intellij.diagnostic.IdePerformanceListener import com.intellij.diagnostic.ThreadDump +import com.intellij.featureStatistics.fusCollectors.LifecycleUsageTriggerCollector import com.intellij.ide.plugins.PluginUtilImpl import com.intellij.openapi.Disposable import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.application.ReadAction import com.intellij.openapi.components.Service import com.intellij.openapi.components.service +import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.extensions.PluginId -import com.intellij.openapi.fileEditor.FileEditorManager import com.intellij.openapi.project.ProjectManager import com.intellij.platform.diagnostic.freezeAnalyzer.FreezeAnalyzer import com.intellij.threadDumpParser.ThreadState import com.intellij.ui.EditorNotifications import java.nio.file.Path +import kotlin.io.path.absolutePathString @Service(Service.Level.APP) internal class PluginFreezeWatcher : IdePerformanceListener, Disposable { @Volatile - var latestFrozenPlugin: PluginId? = null - private val stackTracePattern = """at (\S+)\.(\S+)\(([^:]+):(\d+)\)""".toRegex() + private var latestFrozenPlugin: PluginId? = null + @Volatile + private var lastFreezeDuration: Long = -1 + + private val stackTracePattern: Regex = """at (\S+)\.(\S+)\(([^:]+):(\d+)\)""".toRegex() companion object { @JvmStatic @@ -27,19 +33,63 @@ internal class PluginFreezeWatcher : IdePerformanceListener, Disposable { } init { - ApplicationManager.getApplication().messageBus.connect(this).subscribe(IdePerformanceListener.TOPIC, this) + ApplicationManager.getApplication().messageBus.connect(this) + .subscribe(IdePerformanceListener.TOPIC, this) + } + + fun getFreezeReason(): PluginId? = latestFrozenPlugin + + fun reset() { + latestFrozenPlugin = null + lastFreezeDuration = -1 } override fun dispose() {} + override fun uiFreezeStarted(reportDir: Path) { + if (latestFrozenPlugin != null) return + + lastFreezeDuration = -1 + } + + override fun uiFreezeFinished(durationMs: Long, reportDir: Path?) { + if (lastFreezeDuration < 0) { + lastFreezeDuration = durationMs + + reportCounters() + } + } + override fun dumpedThreads(toFile: Path, dump: ThreadDump) { + if (latestFrozenPlugin != null) return // user have not yet handled previous failure + val freezeCausingThreads = FreezeAnalyzer.analyzeFreeze(dump.rawDump, null)?.threads.orEmpty() val pluginIds = freezeCausingThreads.mapNotNull { analyzeFreezeCausingPlugin(it) } - latestFrozenPlugin = pluginIds.groupingBy { it }.eachCount().maxByOrNull { it.value }?.key + val frozenPlugin = pluginIds.groupingBy { it }.eachCount().maxByOrNull { it.value }?.key ?: return - ProjectManager.getInstance().openProjects.firstOrNull()?.let { project -> - FileEditorManager.getInstance(project).focusedEditor?.file?.let { file -> - EditorNotifications.getInstance(project).updateNotifications(file) + val freezeStorageService = PluginsFreezesService.getInstance() + if (freezeStorageService.shouldBeIgnored(frozenPlugin)) return + freezeStorageService.setLatestFreezeDate(frozenPlugin) + + latestFrozenPlugin = frozenPlugin + + Logger.getInstance(PluginFreezeWatcher::class.java) + .warn("Plugin '$frozenPlugin' caused IDE freeze." + + "Find thread dumps at ${toFile.absolutePathString()}") + + reportCounters() + + ReadAction.compute { + for (project in ProjectManager.getInstance().openProjects) { + EditorNotifications.getInstance(project).updateAllNotifications() + } + } + } + + private fun reportCounters() { + latestFrozenPlugin?.let { + if (lastFreezeDuration > 0) { + LifecycleUsageTriggerCollector.pluginFreezeReported(it, lastFreezeDuration) } } } diff --git a/platform/diagnostic/freezeInPluginNotifier/src/com/intellij/platform/diagnostic/plugin/freeze/PluginsFreezesServiceState.kt b/platform/diagnostic/freezeInPluginNotifier/src/com/intellij/platform/diagnostic/plugin/freeze/PluginsFreezesServiceState.kt index b921ebab0ec7..fccd863d5c4f 100644 --- a/platform/diagnostic/freezeInPluginNotifier/src/com/intellij/platform/diagnostic/plugin/freeze/PluginsFreezesServiceState.kt +++ b/platform/diagnostic/freezeInPluginNotifier/src/com/intellij/platform/diagnostic/plugin/freeze/PluginsFreezesServiceState.kt @@ -3,21 +3,20 @@ package com.intellij.platform.diagnostic.plugin.freeze import com.intellij.openapi.components.* import com.intellij.openapi.extensions.PluginId -import org.jetbrains.annotations.ApiStatus import java.time.Instant import java.time.temporal.ChronoUnit +private const val NOTIFICATION_COOLDOWN_DAYS: Long = 1L + @Service(Service.Level.APP) @State( name = "PluginFreezes", - storages = [Storage(value = "pluginFreezes.xml")] + storages = [Storage(value = "pluginFreezes.xml", roamingType = RoamingType.DISABLED)] ) internal class PluginsFreezesService : PersistentStateComponent { private var state: PluginsFreezesServiceState = PluginsFreezesServiceState() companion object { - private const val NOTIFICATION_COOLDOWN_DAYS = 1L - @JvmStatic fun getInstance(): PluginsFreezesService = service() } @@ -27,14 +26,19 @@ internal class PluginsFreezesService : PersistentStateComponent = mutableMapOf(), +internal data class PluginsFreezesServiceState( + var latestNotificationTime: MutableMap = mutableMapOf(), var mutedPlugins: MutableMap = mutableMapOf(), ) diff --git a/platform/diagnostic/freezeInPluginNotifier/src/com/intellij/platform/diagnostic/plugin/freeze/ResetFreezeNotificationStateAction.kt b/platform/diagnostic/freezeInPluginNotifier/src/com/intellij/platform/diagnostic/plugin/freeze/ResetFreezeNotificationStateAction.kt new file mode 100644 index 000000000000..133212647c3c --- /dev/null +++ b/platform/diagnostic/freezeInPluginNotifier/src/com/intellij/platform/diagnostic/plugin/freeze/ResetFreezeNotificationStateAction.kt @@ -0,0 +1,14 @@ +// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.platform.diagnostic.plugin.freeze + +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.application.WriteAction +import com.intellij.openapi.project.DumbAwareAction + +internal class ResetFreezeNotificationStateAction: DumbAwareAction() { + override fun actionPerformed(e: AnActionEvent) { + WriteAction.run { + PluginsFreezesService.getInstance().reset() + } + } +} \ No newline at end of file diff --git a/platform/platform-impl/src/com/intellij/diagnostic/dropErrorActions.kt b/platform/platform-impl/src/com/intellij/diagnostic/dropErrorActions.kt index edf2fbbe1626..18b5caff5a39 100644 --- a/platform/platform-impl/src/com/intellij/diagnostic/dropErrorActions.kt +++ b/platform/platform-impl/src/com/intellij/diagnostic/dropErrorActions.kt @@ -8,10 +8,7 @@ import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.diagnostic.Attachment import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.project.DumbAwareAction -import com.intellij.openapi.ui.InputValidator -import com.intellij.openapi.ui.Messages import com.intellij.openapi.util.io.FileUtil -import com.intellij.openapi.util.text.StringUtil import com.intellij.util.TimeoutUtil import java.awt.event.ActionEvent.CTRL_MASK import java.awt.event.ActionEvent.SHIFT_MASK @@ -100,27 +97,3 @@ internal class DropAnOutOfMemoryErrorAction : DumbAwareAction() { } } } - -internal class SimulateFreeze : DumbAwareAction() { - override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.BGT - - @Suppress("HardCodedStringLiteral") - override fun actionPerformed(e: AnActionEvent) { - val durationString = Messages.showInputDialog( - e.project, - "Enter freeze duration in ms", - "Freeze Simulator", - null, - "", - object : InputValidator { - override fun checkInput(inputString: String?): Boolean = StringUtil.parseInt(inputString, -1) > 0 - override fun canClose(inputString: String?): Boolean = StringUtil.parseInt(inputString, -1) > 0 - }) ?: return - simulatedFreeze(durationString.toLong()) - } - - // Keep it a function to detect it in EA - private fun simulatedFreeze(ms: Long) { - Thread.sleep(ms) - } -} diff --git a/platform/platform-impl/src/com/intellij/featureStatistics/fusCollectors/LifecycleUsageTriggerCollector.java b/platform/platform-impl/src/com/intellij/featureStatistics/fusCollectors/LifecycleUsageTriggerCollector.java index 8406505a8f7f..8e7b62d3bc02 100644 --- a/platform/platform-impl/src/com/intellij/featureStatistics/fusCollectors/LifecycleUsageTriggerCollector.java +++ b/platform/platform-impl/src/com/intellij/featureStatistics/fusCollectors/LifecycleUsageTriggerCollector.java @@ -8,6 +8,7 @@ import com.intellij.internal.statistic.collectors.fus.MethodNameRuleValidator; import com.intellij.internal.statistic.eventLog.EventLogGroup; import com.intellij.internal.statistic.eventLog.events.*; import com.intellij.internal.statistic.service.fus.collectors.CounterUsagesCollector; +import com.intellij.internal.statistic.utils.PluginInfo; import com.intellij.internal.statistic.utils.StatisticsUploadAssistant; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.diagnostic.Logger; @@ -27,7 +28,7 @@ import static com.intellij.internal.statistic.utils.PluginInfoDetectorKt.getPlug public final class LifecycleUsageTriggerCollector extends CounterUsagesCollector { private static final Logger LOG = Logger.getInstance(LifecycleUsageTriggerCollector.class); - private static final EventLogGroup LIFECYCLE = new EventLogGroup("lifecycle", 70); + private static final EventLogGroup LIFECYCLE = new EventLogGroup("lifecycle", 71); private static final EventField eapField = EventFields.Boolean("eap"); private static final EventField testField = EventFields.Boolean("test"); @@ -63,7 +64,14 @@ public final class LifecycleUsageTriggerCollector extends CounterUsagesCollector private static final EventId FRAME_DEACTIVATED = LIFECYCLE.registerEvent("frame.deactivated"); - private static final EventId1 IDE_FREEZE = LIFECYCLE.registerEvent("ide.freeze", EventFields.Long("duration_ms")); + private static final EventId1 IDE_FREEZE = LIFECYCLE.registerEvent("ide.freeze", EventFields.DurationMs); + + private static final EventId2 IDE_FREEZE_REPORTED_PLUGIN = + LIFECYCLE.registerEvent("ide.freeze.plugin", EventFields.PluginInfo, EventFields.DurationMs); + private static final EventId1 IDE_FREEZE_PLUGIN_DISABLED = + LIFECYCLE.registerEvent("ide.freeze.disabled.plugin", EventFields.PluginInfo); + private static final EventId1 IDE_FREEZE_PLUGIN_IGNORED = + LIFECYCLE.registerEvent("ide.freeze.ignored.plugin", EventFields.PluginInfo); private static final ClassEventField errorField = EventFields.Class("error"); private static final EventField memoryErrorKindField = @@ -80,6 +88,7 @@ public final class LifecycleUsageTriggerCollector extends CounterUsagesCollector private static final EventId IDE_DEADLOCK_DETECTED = LIFECYCLE.registerEvent("ide.deadlock.detected"); + private enum ProjectOpenMode {New, Same, Attach} private static final EventField projectOpenModeField = EventFields.Enum("mode", ProjectOpenMode.class, mode -> Strings.toLowerCase(mode.name())); private static final EventId1 PROJECT_FRAME_SELECTED = LIFECYCLE.registerEvent("project.frame.selected", projectOpenModeField); @@ -219,4 +228,16 @@ public final class LifecycleUsageTriggerCollector extends CounterUsagesCollector public static void onEarlyErrorsIgnored(int numErrors) { EARLY_ERRORS.log(numErrors); } + + public static void pluginFreezeReported(@NotNull PluginId pluginId, long durationMs) { + IDE_FREEZE_REPORTED_PLUGIN.log(getPluginInfoById(pluginId), durationMs); + } + + public static void pluginDisabledOnFreeze(@NotNull PluginId id) { + IDE_FREEZE_PLUGIN_DISABLED.log(getPluginInfoById(id)); + } + + public static void pluginFreezeIgnored(@NotNull PluginId id) { + IDE_FREEZE_PLUGIN_IGNORED.log(getPluginInfoById(id)); + } } diff --git a/platform/platform-resources/src/idea/PlatformActions.xml b/platform/platform-resources/src/idea/PlatformActions.xml index 98216e0ad49f..e03f4b38363d 100644 --- a/platform/platform-resources/src/idea/PlatformActions.xml +++ b/platform/platform-resources/src/idea/PlatformActions.xml @@ -1267,7 +1267,6 @@ - diff --git a/plugins/performanceTesting/core/resources/META-INF/plugin.xml b/plugins/performanceTesting/core/resources/META-INF/plugin.xml index 6d9d6c4ddead..f84cc521996c 100644 --- a/plugins/performanceTesting/core/resources/META-INF/plugin.xml +++ b/plugins/performanceTesting/core/resources/META-INF/plugin.xml @@ -6,9 +6,9 @@ messages.PerformanceTestingBundle + Plugin for automated execution of test scripts, capturing performance snapshots + and gathering performance statistics. + ]]> @@ -33,6 +33,9 @@ + + diff --git a/plugins/performanceTesting/core/src/com/jetbrains/performancePlugin/actions/SimulateFreeze.kt b/plugins/performanceTesting/core/src/com/jetbrains/performancePlugin/actions/SimulateFreeze.kt new file mode 100644 index 000000000000..67cb435dd39a --- /dev/null +++ b/plugins/performanceTesting/core/src/com/jetbrains/performancePlugin/actions/SimulateFreeze.kt @@ -0,0 +1,32 @@ +package com.jetbrains.performancePlugin.actions + +import com.intellij.openapi.actionSystem.ActionUpdateThread +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.project.DumbAwareAction +import com.intellij.openapi.ui.InputValidator +import com.intellij.openapi.ui.Messages +import com.intellij.openapi.util.text.StringUtil + +internal class SimulateFreeze : DumbAwareAction() { + override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.BGT + + @Suppress("HardCodedStringLiteral") + override fun actionPerformed(e: AnActionEvent) { + val durationString = Messages.showInputDialog( + e.project, + "Enter freeze duration in ms", + "Freeze Simulator", + null, + "", + object : InputValidator { + override fun checkInput(inputString: String?): Boolean = StringUtil.parseInt(inputString, -1) > 0 + override fun canClose(inputString: String?): Boolean = StringUtil.parseInt(inputString, -1) > 0 + }) ?: return + simulatedFreeze(durationString.toLong()) + } + + // Keep it a function to detect it in EA + private fun simulatedFreeze(ms: Long) { + Thread.sleep(ms) + } +}