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 @@ + +