diff --git a/.idea/modules.xml b/.idea/modules.xml
index 134d91c10557..67b91983d6ce 100644
--- a/.idea/modules.xml
+++ b/.idea/modules.xml
@@ -11,7 +11,6 @@
-
@@ -634,6 +633,8 @@
+
+
diff --git a/platform/diagnostic/freezeAnalyzer/api-dump-experimental.txt b/platform/diagnostic/freezeAnalyzer/api-dump-experimental.txt
deleted file mode 100644
index 4205e2b4202c..000000000000
--- a/platform/diagnostic/freezeAnalyzer/api-dump-experimental.txt
+++ /dev/null
@@ -1,13 +0,0 @@
-f:com.intellij.platform.diagnostic.freezeAnalyzer.FreezeAnalysisResult
-- (java.lang.String,java.util.List,java.lang.String):V
-- b:(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
diff --git a/platform/diagnostic/freezeAnalyzer/api-dump.txt b/platform/diagnostic/freezeAnalyzer/api-dump.txt
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/platform/diagnostic/freezeAnalyzer/freezeAnalyzer.iml b/platform/diagnostic/freezeAnalyzer/intellij.platform.diagnostic.freezeAnalyzer.iml
similarity index 92%
rename from platform/diagnostic/freezeAnalyzer/freezeAnalyzer.iml
rename to platform/diagnostic/freezeAnalyzer/intellij.platform.diagnostic.freezeAnalyzer.iml
index 87e5368f4735..481e2549b3ca 100644
--- a/platform/diagnostic/freezeAnalyzer/freezeAnalyzer.iml
+++ b/platform/diagnostic/freezeAnalyzer/intellij.platform.diagnostic.freezeAnalyzer.iml
@@ -6,6 +6,7 @@
+
@@ -14,6 +15,5 @@
-
\ No newline at end of file
diff --git a/platform/diagnostic/freezeAnalyzer/resources/intellij.platform.diagnostic.freezeAnalyzer.xml b/platform/diagnostic/freezeAnalyzer/resources/intellij.platform.diagnostic.freezeAnalyzer.xml
new file mode 100644
index 000000000000..bef619d0dfda
--- /dev/null
+++ b/platform/diagnostic/freezeAnalyzer/resources/intellij.platform.diagnostic.freezeAnalyzer.xml
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/platform/diagnostic/freezeAnalyzer/src/com/intellij/platform/diagnostic/freezeAnalyzer/FreezeAnalyzer.kt b/platform/diagnostic/freezeAnalyzer/src/com/intellij/platform/diagnostic/freezeAnalyzer/FreezeAnalyzer.kt
index b2ee77c02fa5..3eff108aa9ed 100644
--- a/platform/diagnostic/freezeAnalyzer/src/com/intellij/platform/diagnostic/freezeAnalyzer/FreezeAnalyzer.kt
+++ b/platform/diagnostic/freezeAnalyzer/src/com/intellij/platform/diagnostic/freezeAnalyzer/FreezeAnalyzer.kt
@@ -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 {
- 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, val additionalMessage: String? = null)
\ No newline at end of file
diff --git a/platform/diagnostic/freezeInPluginNotifier/api-dump.txt b/platform/diagnostic/freezeInPluginNotifier/api-dump.txt
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/platform/diagnostic/freezeInPluginNotifier/intellij.platform.diagnostic.freezes.iml b/platform/diagnostic/freezeInPluginNotifier/intellij.platform.diagnostic.freezes.iml
new file mode 100644
index 000000000000..e149dc717791
--- /dev/null
+++ b/platform/diagnostic/freezeInPluginNotifier/intellij.platform.diagnostic.freezes.iml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ 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
new file mode 100644
index 000000000000..8658cdd38ae9
--- /dev/null
+++ b/platform/diagnostic/freezeInPluginNotifier/resources/intellij.platform.diagnostic.freezes.xml
@@ -0,0 +1,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
new file mode 100644
index 000000000000..cffb9b14d780
--- /dev/null
+++ b/platform/diagnostic/freezeInPluginNotifier/resources/messages/PluginFreezeBundle.properties
@@ -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
\ No newline at end of file
diff --git a/platform/diagnostic/freezeInPluginNotifier/src/com/intellij/platform/diagnostic/plugin/freeze/PluginFreezeBundle.kt b/platform/diagnostic/freezeInPluginNotifier/src/com/intellij/platform/diagnostic/plugin/freeze/PluginFreezeBundle.kt
new file mode 100644
index 000000000000..e1d3448805cd
--- /dev/null
+++ b/platform/diagnostic/freezeInPluginNotifier/src/com/intellij/platform/diagnostic/plugin/freeze/PluginFreezeBundle.kt
@@ -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)
+}
\ 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
new file mode 100644
index 000000000000..c2e4a1201359
--- /dev/null
+++ b/platform/diagnostic/freezeInPluginNotifier/src/com/intellij/platform/diagnostic/plugin/freeze/PluginFreezeNotifier.kt
@@ -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? {
+ 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) }
+ }
+
+}
\ 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
new file mode 100644
index 000000000000..2ba0068a99e5
--- /dev/null
+++ b/platform/diagnostic/freezeInPluginNotifier/src/com/intellij/platform/diagnostic/plugin/freeze/PluginFreezeWatcher.kt
@@ -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())
+ }
+ }
+}
\ No newline at end of file
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
new file mode 100644
index 000000000000..b4a4b7e289a7
--- /dev/null
+++ b/platform/diagnostic/freezeInPluginNotifier/src/com/intellij/platform/diagnostic/plugin/freeze/PluginsFreezesServiceState.kt
@@ -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 {
+ 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 = mutableMapOf(),
+ var mutedPlugins: MutableMap = mutableMapOf(),
+)
diff --git a/platform/platform-resources/src/META-INF/common-ide-modules.xml b/platform/platform-resources/src/META-INF/common-ide-modules.xml
index 0fb4434ae941..8647b676d80c 100644
--- a/platform/platform-resources/src/META-INF/common-ide-modules.xml
+++ b/platform/platform-resources/src/META-INF/common-ide-modules.xml
@@ -11,6 +11,8 @@
+
+