mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-03-22 06:50:54 +07:00
IJPL-161370 Detect UI freezes caused by third-party plugins and disable them
GitOrigin-RevId: f46bffd14faabe761b2f4a5b1920250c22a0e349
This commit is contained in:
committed by
intellij-monorepo-bot
parent
f7eeedb6a1
commit
e67615d12d
3
.idea/modules.xml
generated
3
.idea/modules.xml
generated
@@ -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" />
|
||||
|
||||
@@ -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
|
||||
0
platform/diagnostic/freezeAnalyzer/api-dump.txt
Normal file
0
platform/diagnostic/freezeAnalyzer/api-dump.txt
Normal 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>
|
||||
@@ -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>
|
||||
@@ -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)
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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) }
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
)
|
||||
@@ -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"/>
|
||||
|
||||
Reference in New Issue
Block a user