[performance] IJPL-161370 Detect UI freezes caused by third-party plugins and disable them

(cherry picked from commit 57013d91d5767602b1cb6db72d644e83bed2339b)
IJ-CR-146685

GitOrigin-RevId: e1a46a784e5c742410087590b1013a6d553f415b
This commit is contained in:
Yuriy Artamonov
2024-10-17 10:43:52 +02:00
committed by intellij-monorepo-bot
parent d47bffbfe0
commit 22b4b596ea
12 changed files with 162 additions and 60 deletions

View File

@@ -17,5 +17,6 @@
<orderEntry type="module" module-name="intellij.platform.ide.impl" />
<orderEntry type="library" name="kotlinx-serialization-json" level="project" />
<orderEntry type="library" name="kotlinx-serialization-core" level="project" />
<orderEntry type="module" module-name="intellij.platform.statistics" />
</component>
</module>

View File

@@ -6,4 +6,9 @@
<extensions defaultExtensionNs="com.intellij">
<editorNotificationProvider implementation="com.intellij.platform.diagnostic.plugin.freeze.PluginFreezeNotifier"/>
</extensions>
<actions>
<action id="ResetFreezeNotificationState" class="com.intellij.platform.diagnostic.plugin.freeze.ResetFreezeNotificationStateAction"
internal="true" text="Reset Freezes Notification State"/>
</actions>
</idea-plugin>

View File

@@ -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
notification.content.plugin.caused.freeze.detected=Plugin ''{0}'' might be slowing things down

View File

@@ -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<in FileEditor, out JComponent?>? {
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()
}
}

View File

@@ -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<Unit, Throwable> {
for (project in ProjectManager.getInstance().openProjects) {
EditorNotifications.getInstance(project).updateAllNotifications()
}
}
}
private fun reportCounters() {
latestFrozenPlugin?.let {
if (lastFreezeDuration > 0) {
LifecycleUsageTriggerCollector.pluginFreezeReported(it, lastFreezeDuration)
}
}
}

View File

@@ -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<PluginsFreezesServiceState> {
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<PluginsFreezesSe
}
fun setLatestFreezeDate(pluginId: PluginId) {
state.latestNotificationData[pluginId.idString] = Instant.now().toString()
state.latestNotificationTime[pluginId.idString] = Instant.now().toString()
}
fun reset() {
state.latestNotificationTime.clear()
state.mutedPlugins.clear()
}
fun shouldBeIgnored(pluginId: PluginId): Boolean {
val pluginIdString = pluginId.idString
if (state.mutedPlugins[pluginIdString] == true) return true
val lastNotification = state.latestNotificationData[pluginIdString]?.let { Instant.parse(it) } ?: return false
val lastNotification = state.latestNotificationTime[pluginIdString]?.let { Instant.parse(it) } ?: return false
return Instant.now().isBefore(lastNotification.plus(NOTIFICATION_COOLDOWN_DAYS, ChronoUnit.DAYS))
}
@@ -45,8 +49,7 @@ internal class PluginsFreezesService : PersistentStateComponent<PluginsFreezesSe
}
}
@ApiStatus.Internal
data class PluginsFreezesServiceState(
var latestNotificationData: MutableMap<String, String> = mutableMapOf(),
internal data class PluginsFreezesServiceState(
var latestNotificationTime: MutableMap<String, String> = mutableMapOf(),
var mutedPlugins: MutableMap<String, Boolean> = mutableMapOf(),
)

View File

@@ -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<RuntimeException> {
PluginsFreezesService.getInstance().reset()
}
}
}

View File

@@ -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)
}
}

View File

@@ -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<Boolean> eapField = EventFields.Boolean("eap");
private static final EventField<Boolean> 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<Long> IDE_FREEZE = LIFECYCLE.registerEvent("ide.freeze", EventFields.Long("duration_ms"));
private static final EventId1<Long> IDE_FREEZE = LIFECYCLE.registerEvent("ide.freeze", EventFields.DurationMs);
private static final EventId2<PluginInfo, Long> IDE_FREEZE_REPORTED_PLUGIN =
LIFECYCLE.registerEvent("ide.freeze.plugin", EventFields.PluginInfo, EventFields.DurationMs);
private static final EventId1<PluginInfo> IDE_FREEZE_PLUGIN_DISABLED =
LIFECYCLE.registerEvent("ide.freeze.disabled.plugin", EventFields.PluginInfo);
private static final EventId1<PluginInfo> IDE_FREEZE_PLUGIN_IGNORED =
LIFECYCLE.registerEvent("ide.freeze.ignored.plugin", EventFields.PluginInfo);
private static final ClassEventField errorField = EventFields.Class("error");
private static final EventField<VMOptions.MemoryKind> 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<ProjectOpenMode> projectOpenModeField = EventFields.Enum("mode", ProjectOpenMode.class, mode -> Strings.toLowerCase(mode.name()));
private static final EventId1<ProjectOpenMode> 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));
}
}

View File

@@ -1267,7 +1267,6 @@
<action id="DropAnOutOfMemoryError" internal="true" class="com.intellij.diagnostic.DropAnOutOfMemoryErrorAction"
text="Drop an OutOfMemoryError" description="Hold down SHIFT for OOME in Metaspace"/>
</group>
<action id="SimulateFreeze" internal="true" class="com.intellij.diagnostic.SimulateFreeze" text="Simulate a Freeze"/>
<separator/>
<action internal="true" id="ReloadProjectAction" class="com.intellij.internal.ReloadProjectAction"/>
<group id="Internal.Trust" popup="true">

View File

@@ -6,9 +6,9 @@
<resource-bundle>messages.PerformanceTestingBundle</resource-bundle>
<description><![CDATA[
Plugin for automated execution of test scripts, capturing performance snapshots
and gathering performance statistics.
]]></description>
Plugin for automated execution of test scripts, capturing performance snapshots
and gathering performance statistics.
]]></description>
<content>
<module name="intellij.performanceTesting.remoteDriver"/>
@@ -33,6 +33,9 @@
<separator/>
<add-to-group group-id="HelpDiagnosticTools" anchor="last"/>
</group>
<action id="SimulateFreeze" internal="true" class="com.jetbrains.performancePlugin.actions.SimulateFreeze"
text="Simulate a Freeze"/>
</actions>
<extensionPoints>

View File

@@ -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)
}
}