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

GitOrigin-RevId: f46bffd14faabe761b2f4a5b1920250c22a0e349
This commit is contained in:
Maxim.Kolmakov
2024-10-11 13:07:38 +02:00
committed by intellij-monorepo-bot
parent f7eeedb6a1
commit e67615d12d
15 changed files with 239 additions and 21 deletions

3
.idea/modules.xml generated
View File

@@ -11,7 +11,6 @@
<module fileurl="file://$PROJECT_DIR$/fleet/util/core/fleet.util.core.iml" filepath="$PROJECT_DIR$/fleet/util/core/fleet.util.core.iml" />
<module fileurl="file://$PROJECT_DIR$/fleet/util/logging/api/fleet.util.logging.api.iml" filepath="$PROJECT_DIR$/fleet/util/logging/api/fleet.util.logging.api.iml" />
<module fileurl="file://$PROJECT_DIR$/fleet/util/os/fleet.util.os.iml" filepath="$PROJECT_DIR$/fleet/util/os/fleet.util.os.iml" />
<module fileurl="file://$PROJECT_DIR$/platform/diagnostic/freezeAnalyzer/freezeAnalyzer.iml" filepath="$PROJECT_DIR$/platform/diagnostic/freezeAnalyzer/freezeAnalyzer.iml" />
<module fileurl="file://$PROJECT_DIR$/plugins/feature-usage-database/plugin-community/intellij.ae.database.community.iml" filepath="$PROJECT_DIR$/plugins/feature-usage-database/plugin-community/intellij.ae.database.community.iml" />
<module fileurl="file://$PROJECT_DIR$/plugins/feature-usage-database/core/intellij.ae.database.core.iml" filepath="$PROJECT_DIR$/plugins/feature-usage-database/core/intellij.ae.database.core.iml" />
<module fileurl="file://$PROJECT_DIR$/plugins/feature-usage-database/counters/intellij.ae.database.counters.community.iml" filepath="$PROJECT_DIR$/plugins/feature-usage-database/counters/intellij.ae.database.counters.community.iml" />
@@ -634,6 +633,8 @@
<module fileurl="file://$PROJECT_DIR$/platform/xdebugger-impl/frontend/intellij.platform.debugger.impl.frontend.iml" filepath="$PROJECT_DIR$/platform/xdebugger-impl/frontend/intellij.platform.debugger.impl.frontend.iml" />
<module fileurl="file://$PROJECT_DIR$/platform/xdebugger-testFramework/intellij.platform.debugger.testFramework.iml" filepath="$PROJECT_DIR$/platform/xdebugger-testFramework/intellij.platform.debugger.testFramework.iml" />
<module fileurl="file://$PROJECT_DIR$/platform/diagnostic/intellij.platform.diagnostic.iml" filepath="$PROJECT_DIR$/platform/diagnostic/intellij.platform.diagnostic.iml" />
<module fileurl="file://$PROJECT_DIR$/platform/diagnostic/freezeAnalyzer/intellij.platform.diagnostic.freezeAnalyzer.iml" filepath="$PROJECT_DIR$/platform/diagnostic/freezeAnalyzer/intellij.platform.diagnostic.freezeAnalyzer.iml" />
<module fileurl="file://$PROJECT_DIR$/platform/diagnostic/freezeInPluginNotifier/intellij.platform.diagnostic.freezes.iml" filepath="$PROJECT_DIR$/platform/diagnostic/freezeInPluginNotifier/intellij.platform.diagnostic.freezes.iml" />
<module fileurl="file://$PROJECT_DIR$/platform/diagnostic/startUpPerformanceReporter/intellij.platform.diagnostic.startUpPerformanceReporter.iml" filepath="$PROJECT_DIR$/platform/diagnostic/startUpPerformanceReporter/intellij.platform.diagnostic.startUpPerformanceReporter.iml" />
<module fileurl="file://$PROJECT_DIR$/platform/diagnostic/telemetry/intellij.platform.diagnostic.telemetry.iml" filepath="$PROJECT_DIR$/platform/diagnostic/telemetry/intellij.platform.diagnostic.telemetry.iml" />
<module fileurl="file://$PROJECT_DIR$/platform/diagnostic/telemetry.exporters/intellij.platform.diagnostic.telemetry.exporters.iml" filepath="$PROJECT_DIR$/platform/diagnostic/telemetry.exporters/intellij.platform.diagnostic.telemetry.exporters.iml" />

View File

@@ -1,13 +0,0 @@
f:com.intellij.platform.diagnostic.freezeAnalyzer.FreezeAnalysisResult
- <init>(java.lang.String,java.util.List,java.lang.String):V
- b:<init>(java.lang.String,java.util.List,java.lang.String,I,kotlin.jvm.internal.DefaultConstructorMarker):V
- f:component1():java.lang.String
- f:component2():java.util.List
- f:component3():java.lang.String
- f:copy(java.lang.String,java.util.List,java.lang.String):com.intellij.platform.diagnostic.freezeAnalyzer.FreezeAnalysisResult
- bs:copy$default(com.intellij.platform.diagnostic.freezeAnalyzer.FreezeAnalysisResult,java.lang.String,java.util.List,java.lang.String,I,java.lang.Object):com.intellij.platform.diagnostic.freezeAnalyzer.FreezeAnalysisResult
- equals(java.lang.Object):Z
- f:getAdditionalMessage():java.lang.String
- f:getMessage():java.lang.String
- f:getThreads():java.util.List
- hashCode():I

View File

@@ -6,6 +6,7 @@
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/testData" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/testSrc" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/resources" type="java-resource" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
@@ -14,6 +15,5 @@
<orderEntry type="library" scope="TEST" name="kotlin-test-assertions-core-jvm" level="project" />
<orderEntry type="library" scope="TEST" name="JUnit5" level="project" />
<orderEntry type="library" name="jetbrains-annotations" level="project" />
<orderEntry type="module" module-name="intellij.platform.util.base" />
</component>
</module>

View File

@@ -0,0 +1,3 @@
<!--The code is shared with tests, it must not depend on anything-->
<idea-plugin package="com.intellij.platform.diagnostic.freezeAnalyzer">
</idea-plugin>

View File

@@ -1,17 +1,12 @@
package com.intellij.platform.diagnostic.freezeAnalyzer
import com.intellij.diagnostic.ThreadDump
import com.intellij.threadDumpParser.ThreadDumpParser
import com.intellij.threadDumpParser.ThreadState
import org.jetbrains.annotations.ApiStatus
@ApiStatus.Experimental
@ApiStatus.Internal
object FreezeAnalyzer {
fun getRelevantThreads(threadDump: ThreadDump): List<ThreadState> {
return analyzeFreeze(threadDump.rawDump, null)?.threads ?: emptyList()
}
/**
* Analyze freeze based on the IJ Platform knowledge and try to infer the relevant message.
* If analysis fails, it returns `null`.
@@ -165,4 +160,5 @@ object FreezeAnalyzer {
}
}
@ApiStatus.Internal
data class FreezeAnalysisResult(val message: String, val threads: List<ThreadState>, val additionalMessage: String? = null)

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/resources" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="kotlin-stdlib" level="project" />
<orderEntry type="module" module-name="intellij.platform.core" />
<orderEntry type="module" module-name="intellij.platform.threadDumpParser" />
<orderEntry type="module" module-name="intellij.platform.diagnostic.freezeAnalyzer" />
<orderEntry type="module" module-name="intellij.platform.core.impl" />
<orderEntry type="module" module-name="intellij.platform.ide" />
<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" />
</component>
</module>

View File

@@ -0,0 +1,9 @@
<idea-plugin package="com.intellij.platform.diagnostic.plugin.freeze">
<dependencies>
<module name="intellij.platform.diagnostic.freezeAnalyzer"/>
</dependencies>
<extensions defaultExtensionNs="com.intellij">
<editorNotificationProvider implementation="com.intellij.platform.diagnostic.plugin.freeze.PluginFreezeNotifier"/>
</extensions>
</idea-plugin>

View File

@@ -0,0 +1,5 @@
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=Plugin {0} caused the freeze
notification.group.plugin.freeze=Freeze in 3rd-party plugin detected

View File

@@ -0,0 +1,17 @@
package com.intellij.platform.diagnostic.plugin.freeze
import com.intellij.DynamicBundle
import org.jetbrains.annotations.ApiStatus
import org.jetbrains.annotations.Nls
import org.jetbrains.annotations.PropertyKey
@ApiStatus.Internal
object PluginFreezeBundle {
private const val BUNDLE: String = "messages.PluginFreezeBundle"
private val INSTANCE: DynamicBundle = DynamicBundle(PluginFreezeBundle::class.java, BUNDLE)
@JvmStatic
fun message(@PropertyKey(resourceBundle = BUNDLE) key: String, vararg params: Any): @Nls String =
INSTANCE.getMessage(key, *params)
}

View File

@@ -0,0 +1,61 @@
// 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.ide.plugins.PluginManagerCore
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
import com.intellij.ui.EditorNotificationProvider
import com.intellij.ui.EditorNotifications
import com.intellij.util.ui.RestartDialogImpl
import org.jetbrains.annotations.ApiStatus
import java.util.function.Function
import javax.swing.JComponent
@ApiStatus.Internal
class PluginFreezeNotifier : EditorNotificationProvider {
private val freezeWatcher = PluginFreezeWatcher.getInstance()
private val freezeStorageService = PluginsFreezesService.getInstance()
override fun collectNotificationData(project: Project, file: VirtualFile): Function<in FileEditor, out JComponent?>? {
val frozenPlugin = freezeWatcher.latestFrozenPlugin ?: return null
val pluginDescriptor = PluginManagerCore.getPlugin(frozenPlugin) ?: return null
if (pluginDescriptor.isBundled) 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)
}
createActionLabel(PluginFreezeBundle.message("action.ignore.plugin.text")) {
freezeStorageService.mutePlugin(frozenPlugin)
closePanel(project)
}
createActionLabel(PluginFreezeBundle.message("action.close.panel.text")) {
closePanel(project)
}
}
}
}
//TODO support dynamic plugins
private fun disablePlugin(frozenPlugin: PluginId) {
val pluginDisabled = PluginManagerCore.disablePlugin(frozenPlugin)
if (pluginDisabled) {
RestartDialogImpl.showRestartRequired()
}
}
private fun closePanel(project: Project) {
freezeWatcher.latestFrozenPlugin = null
FileEditorManager.getInstance(project).openFiles.forEach { it -> EditorNotifications.getInstance(project).updateNotifications(it) }
}
}

View File

@@ -0,0 +1,63 @@
package com.intellij.platform.diagnostic.plugin.freeze
import com.intellij.diagnostic.IdePerformanceListener
import com.intellij.diagnostic.ThreadDump
import com.intellij.ide.plugins.PluginUtilImpl
import com.intellij.openapi.Disposable
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.components.Service
import com.intellij.openapi.components.service
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 org.jetbrains.annotations.ApiStatus
import java.nio.file.Path
@Service(Service.Level.APP)
@ApiStatus.Internal
class PluginFreezeWatcher : IdePerformanceListener, Disposable {
var latestFrozenPlugin: PluginId? = null
private val stackTracePattern = """at (\S+)\.(\S+)\(([^:]+):(\d+)\)""".toRegex()
companion object {
@JvmStatic
fun getInstance(): PluginFreezeWatcher = service()
}
init {
ApplicationManager.getApplication().messageBus.connect(this).subscribe(IdePerformanceListener.TOPIC, this)
}
override fun dispose() {}
override fun dumpedThreads(toFile: Path, dump: ThreadDump) {
val freezeCausingThreads = FreezeAnalyzer.analyzeFreeze(dump.rawDump, null)?.threads.orEmpty()
val pluginIds = freezeCausingThreads.mapNotNull { analyzeFreezeCausingPlugin(it) }
latestFrozenPlugin = pluginIds.groupingBy { it }.eachCount().maxByOrNull { it.value }?.key
ProjectManager.getInstance().openProjects.firstOrNull()?.let { project ->
FileEditorManager.getInstance(project).focusedEditor?.file?.let { file ->
EditorNotifications.getInstance(project).updateNotifications(file)
}
}
}
private fun analyzeFreezeCausingPlugin(threadInfo: ThreadState): PluginId? {
val stackTraceElements = threadInfo.stackTrace.lineSequence()
.mapNotNull { parseStackTraceElement(it) }
.toList()
.toTypedArray()
return PluginUtilImpl.doFindPluginId(Throwable().apply { stackTrace = stackTraceElements })
}
private fun parseStackTraceElement(stackTrace: String): StackTraceElement? {
return stackTracePattern.find(stackTrace.trim())?.let { matchResult ->
val (className, methodName, fileName, lineNumber) = matchResult.destructured
StackTraceElement(className, methodName, fileName, lineNumber.toInt())
}
}
}

View File

@@ -0,0 +1,53 @@
// 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.components.*
import com.intellij.openapi.extensions.PluginId
import org.jetbrains.annotations.ApiStatus
import java.time.Instant
import java.time.temporal.ChronoUnit
@Service(Service.Level.APP)
@State(
name = "PluginFreezes",
storages = [Storage(value = "pluginFreezes.xml")]
)
@ApiStatus.Internal
class PluginsFreezesService : PersistentStateComponent<PluginsFreezesServiceState> {
private var state: PluginsFreezesServiceState = PluginsFreezesServiceState()
companion object {
private const val NOTIFICATION_COOLDOWN_DAYS = 1L
@JvmStatic
fun getInstance(): PluginsFreezesService = service()
}
fun mutePlugin(pluginId: PluginId) {
state.mutedPlugins[pluginId.idString] = true
}
fun setLatestFreezeDate(pluginId: PluginId) {
state.latestNotificationData[pluginId.idString] = Instant.now().toString()
}
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
return Instant.now().isBefore(lastNotification.plus(NOTIFICATION_COOLDOWN_DAYS, ChronoUnit.DAYS))
}
override fun getState(): PluginsFreezesServiceState = state
override fun loadState(state: PluginsFreezesServiceState) {
this.state = state
}
}
@ApiStatus.Internal
data class PluginsFreezesServiceState(
var latestNotificationData: MutableMap<String, String> = mutableMapOf(),
var mutedPlugins: MutableMap<String, Boolean> = mutableMapOf(),
)

View File

@@ -11,6 +11,8 @@
<module name="intellij.libraries.skiko"/>
<module name="intellij.libraries.compose.desktop"/>
<module name="intellij.platform.compose"/>
<module name="intellij.platform.diagnostic.freezeAnalyzer"/>
<module name="intellij.platform.diagnostic.freezes"/>
</content>
<xi:include href="/META-INF/vcs-modules.xml"/>