IJPL-164498 Provide the ability to see thread dump from "plugin might be slowing down" notification

(cherry picked from commit 221a494ba04c3bc696efdf39d7962e3d23eb88fb)
IJ-CR-146685

GitOrigin-RevId: 913f93c2c4381878807efd2b05cb800882aef97a
This commit is contained in:
Yuriy Artamonov
2024-10-21 23:54:33 +02:00
committed by intellij-monorepo-bot
parent 9dbc6953c2
commit 2fa39514f1
11 changed files with 189 additions and 172 deletions

View File

@@ -39,5 +39,6 @@
<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" />
<orderEntry type="module" module-name="intellij.platform.core.ui" />
</component>
</module>

View File

@@ -4,7 +4,9 @@
</dependencies>
<extensions defaultExtensionNs="com.intellij">
<editorNotificationProvider implementation="com.intellij.platform.diagnostic.plugin.freeze.PluginFreezeNotifier"/>
<diagnostic.freezeNotifier implementation="com.intellij.platform.diagnostic.plugin.freeze.PluginFreezeNotifier"/>
<editorNotificationProvider implementation="com.intellij.platform.diagnostic.plugin.freeze.PluginFreezeNotificationPanel"/>
<registryKey key="ide.diagnostics.notification.freezes.in.plugins"
defaultValue="true"
description="Show notification about freezes in plugins"/>

View File

@@ -1,6 +1,7 @@
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}'' might be slowing things down
action.open.issue.tracker.text=Open issue tracker
progress.title.opening.issue.tracker=Opening plugin issue tracker
action.ignore.plugin.text=Ignore
notification.content.freeze.detected={0} has encountered a slowdown
notification.content.plugin.caused.freeze=Plugin ''{0}'' might be slowing things down
action.report.text=Report problem
action.ignore.plugin.tooltip=Ignore such performance issues in the future
action.dismiss.tooltip=Dismiss the notification

View File

@@ -0,0 +1,116 @@
// 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.diagnostic.FreezeNotifier
import com.intellij.diagnostic.IdeErrorsDialog
import com.intellij.diagnostic.MessagePool
import com.intellij.diagnostic.ThreadDump
import com.intellij.featureStatistics.fusCollectors.LifecycleUsageTriggerCollector
import com.intellij.ide.plugins.PluginManagerCore
import com.intellij.ide.plugins.PluginManagerCore.isVendorJetBrains
import com.intellij.openapi.application.impl.ApplicationInfoImpl
import com.intellij.openapi.diagnostic.IdeaLoggingEvent
import com.intellij.openapi.extensions.PluginDescriptor
import com.intellij.openapi.fileEditor.FileEditor
import com.intellij.openapi.project.Project
import com.intellij.openapi.project.ProjectManager
import com.intellij.openapi.util.registry.Registry
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.ui.EditorNotificationPanel
import com.intellij.ui.EditorNotificationProvider
import com.intellij.ui.EditorNotifications
import java.nio.file.Path
import java.util.*
import java.util.function.Function
import javax.swing.JComponent
internal class PluginFreezeNotifier: FreezeNotifier {
override fun notifyFreeze(event: IdeaLoggingEvent, currentDumps: Collection<ThreadDump>, reportDir: Path, durationMs: Long) {
val freezeWatcher = PluginFreezeWatcher.getInstance()
val freezeReason = freezeWatcher.getFreezeReason()
if (freezeReason != null) return // still have previous reason shown to user
for (dump in currentDumps) {
val reason = freezeWatcher.dumpedThreads(event, dump, durationMs)
if (reason != null) {
LifecycleUsageTriggerCollector.pluginFreezeDetected(reason.pluginId, durationMs)
reportFreeze()
break
}
}
}
private fun reportFreeze() {
for (project in ProjectManager.getInstance().openProjects) {
EditorNotifications.getInstance(project).updateAllNotifications()
}
}
}
internal class PluginFreezeNotificationPanel : EditorNotificationProvider {
private val reported: MutableSet<FreezeReason> = Collections.synchronizedSet(HashSet())
override fun collectNotificationData(project: Project, file: VirtualFile): Function<in FileEditor, out JComponent?>? {
if (!Registry.get("ide.diagnostics.notification.freezes.in.plugins").asBoolean()) return null
val freezeReason = PluginFreezeWatcher.getInstance().getFreezeReason() ?: return null
val frozenPlugin = freezeReason.pluginId
val pluginDescriptor = PluginManagerCore.getPlugin(frozenPlugin) ?: return null
return Function {
EditorNotificationPanel(EditorNotificationPanel.Status.Warning).apply {
text = if (isVendorJetBrains(pluginDescriptor.vendor ?: "")) {
PluginFreezeBundle.message("notification.content.freeze.detected", ApplicationInfoImpl.getShadowInstance().versionName)
}
else {
PluginFreezeBundle.message("notification.content.plugin.caused.freeze", pluginDescriptor.name)
}
createActionLabel(PluginFreezeBundle.message("action.report.text")) {
reportFreeze(project, pluginDescriptor, freezeReason)
}
createActionLabel(PluginFreezeBundle.message("action.ignore.plugin.text")) {
PluginsFreezesService.getInstance().mutePlugin(frozenPlugin)
LifecycleUsageTriggerCollector.pluginFreezeIgnored(frozenPlugin)
closePanel(project)
}.apply {
toolTipText = PluginFreezeBundle.message("action.ignore.plugin.tooltip")
}
createActionLabel(PluginFreezeBundle.message("action.close.panel.text")) {
closePanel(project)
}.apply {
toolTipText = PluginFreezeBundle.message("action.dismiss.tooltip")
}
}
}
}
private fun reportFreeze(project: Project, pluginDescriptor: PluginDescriptor, freezeReason: FreezeReason) {
if (reported.add(freezeReason)) {
// must be added only once
MessagePool.getInstance().addIdeFatalMessage(freezeReason.event)
}
val dialog = object : IdeErrorsDialog(MessagePool.getInstance(), project, null) {
override fun updateOnSubmit() {
super.updateOnSubmit()
LifecycleUsageTriggerCollector.pluginFreezeReported(pluginDescriptor.pluginId)
closePanel(project)
}
}
dialog.show()
}
private fun closePanel(project: Project) {
PluginFreezeWatcher.getInstance().reset()
reported.clear()
EditorNotifications.getInstance(project).updateAllNotifications()
}
}

View File

@@ -1,83 +0,0 @@
// 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.BrowserUtil
import com.intellij.ide.plugins.IdeaPluginDescriptor
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.project.Project
import com.intellij.openapi.util.registry.Registry
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.platform.ide.progress.runWithModalProgressBlocking
import com.intellij.ui.EditorNotificationPanel
import com.intellij.ui.EditorNotificationProvider
import com.intellij.ui.EditorNotifications
import com.intellij.util.ui.RestartDialogImpl
import java.util.function.Function
import javax.swing.JComponent
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?>? {
if (!Registry.get("ide.diagnostics.notification.freezes.in.plugins").asBoolean()) 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.isInternal) return null
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.open.issue.tracker.text")) {
openIssueTracker(project, pluginDescriptor)
LifecycleUsageTriggerCollector.pluginIssueTrackerOpened()
}
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 openIssueTracker(project: Project, pluginDescriptor: IdeaPluginDescriptor) {
runWithModalProgressBlocking(project, PluginFreezeBundle.message("progress.title.opening.issue.tracker")) {
PluginIssueTrackerResolver.getMarketplaceBugTrackerUrl(pluginDescriptor)?.let {
BrowserUtil.open(it)
}
}
}
private fun closePanel(project: Project) {
freezeWatcher.reset()
EditorNotifications.getInstance(project).updateAllNotifications()
}
}

View File

@@ -1,29 +1,21 @@
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.PluginManagerCore
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.application.impl.ApplicationInfoImpl
import com.intellij.openapi.components.Service
import com.intellij.openapi.components.service
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.diagnostic.IdeaLoggingEvent
import com.intellij.openapi.extensions.PluginId
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 {
internal class PluginFreezeWatcher {
@Volatile
private var latestFrozenPlugin: PluginId? = null
@Volatile
private var lastFreezeDuration: Long = -1
private var reason: FreezeReason? = null
private val stackTracePattern: Regex = """at (\S+)\.(\S+)\(([^:]+):(\d+)\)""".toRegex()
@@ -32,66 +24,29 @@ internal class PluginFreezeWatcher : IdePerformanceListener, Disposable {
fun getInstance(): PluginFreezeWatcher = service()
}
init {
ApplicationManager.getApplication().messageBus.connect(this)
.subscribe(IdePerformanceListener.TOPIC, this)
}
fun getFreezeReason(): PluginId? = latestFrozenPlugin
fun getFreezeReason(): FreezeReason? = reason
fun reset() {
latestFrozenPlugin = null
lastFreezeDuration = -1
reason = null
}
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
fun dumpedThreads(event: IdeaLoggingEvent, dump: ThreadDump, durationMs: Long) : FreezeReason? {
val freezeCausingThreads = FreezeAnalyzer.analyzeFreeze(dump.rawDump, null)?.threads.orEmpty()
val pluginIds = freezeCausingThreads.mapNotNull { analyzeFreezeCausingPlugin(it) }
val frozenPlugin = pluginIds.groupingBy { it }.eachCount().maxByOrNull { it.value }?.key ?: return
val frozenPlugin = pluginIds.groupingBy { it }.eachCount().maxByOrNull { it.value }?.key ?: return null
val pluginDescriptor = PluginManagerCore.getPlugin(frozenPlugin) ?: return null
if (pluginDescriptor.isImplementationDetail || ApplicationInfoImpl.getShadowInstance().isEssentialPlugin(frozenPlugin)) return null
if (pluginDescriptor.isBundled && !ApplicationManager.getApplication().isInternal) return null
val freezeStorageService = PluginsFreezesService.getInstance()
if (freezeStorageService.shouldBeIgnored(frozenPlugin)) return
if (freezeStorageService.shouldBeIgnored(frozenPlugin)) return null
freezeStorageService.setLatestFreezeDate(frozenPlugin)
latestFrozenPlugin = frozenPlugin
reason = FreezeReason(frozenPlugin, event, durationMs)
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)
}
}
return reason
}
private fun analyzeFreezeCausingPlugin(threadInfo: ThreadState): PluginId? {
@@ -109,4 +64,10 @@ internal class PluginFreezeWatcher : IdePerformanceListener, Disposable {
StackTraceElement(className, methodName, fileName, lineNumber.toInt())
}
}
}
}
internal data class FreezeReason(
val pluginId: PluginId,
val event: IdeaLoggingEvent,
val durationMs: Long
)

View File

@@ -0,0 +1,11 @@
// 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.diagnostic
import com.intellij.openapi.diagnostic.IdeaLoggingEvent
import org.jetbrains.annotations.ApiStatus
import java.nio.file.Path
@ApiStatus.Internal
interface FreezeNotifier {
fun notifyFreeze(event: IdeaLoggingEvent, currentDumps: Collection<ThreadDump>, reportDir: Path, durationMs: Long)
}

View File

@@ -68,7 +68,7 @@ import javax.swing.event.DocumentEvent
import javax.swing.event.HyperlinkEvent
import javax.swing.text.JTextComponent
open class IdeErrorsDialog internal constructor(
open class IdeErrorsDialog @ApiStatus.Internal constructor(
private val myMessagePool: MessagePool,
private val myProject: Project?,
defaultMessage: LogMessage?

View File

@@ -34,9 +34,11 @@ import kotlin.coroutines.coroutineContext
import kotlin.math.max
import kotlin.math.min
private val FREEZE_NOTIFIER_EP: ExtensionPointName<FreezeNotifier> = ExtensionPointName("com.intellij.diagnostic.freezeNotifier")
internal class IdeaFreezeReporter : PerformanceListener {
private var dumpTask: SamplingTask? = null
private val currentDumps = ArrayList<ThreadDump>()
private val currentDumps = Collections.synchronizedList(ArrayList<ThreadDump>())
private var stacktraceCommonPart: List<StackTraceElement>? = null
@Volatile
@@ -165,19 +167,30 @@ internal class IdeaFreezeReporter : PerformanceListener {
return
}
val dumps = ArrayList(currentDumps) // defensive copy
if (Registry.`is`("freeze.reporter.enabled", false)) {
val performanceWatcher = PerformanceWatcher.getInstance()
// check that we have at least half of the dumps required
if ((durationMs / 1000).toInt() > FREEZE_THRESHOLD && !stacktraceCommonPart.isNullOrEmpty()) {
val dumpingDurationMs = durationMs - performanceWatcher.unresponsiveInterval
val dumpsCount = min(performanceWatcher.maxDumpDuration.toLong(), dumpingDurationMs / 2) / performanceWatcher.dumpInterval
if (dumpTask.isValid(dumpingDurationMs) || currentDumps.size >= max(3, dumpsCount)) {
if (dumpTask.isValid(dumpingDurationMs) || dumps.size >= max(3, dumpsCount)) {
val attachments = ArrayList<Attachment>()
addDumpsAttachments(from = currentDumps, textMapper = { it.rawDump }, container = attachments)
addDumpsAttachments(from = dumps, textMapper = { it.rawDump }, container = attachments)
if (reportDir != null) {
EP_NAME.forEachExtensionSafe { attachments.addAll(it.getAttachments(reportDir)) }
}
report(createEvent(dumpTask, durationMs, attachments, reportDir, performanceWatcher, finished = true))
val loggingEvent = createEvent(dumpTask, durationMs, attachments, reportDir, performanceWatcher, finished = true)
report(loggingEvent)
if (reportDir != null && loggingEvent != null && dumps.isNotEmpty()) {
for (notifier in FREEZE_NOTIFIER_EP.extensionList) {
notifier.notifyFreeze(loggingEvent, dumps, reportDir, durationMs)
}
}
}
}
}

View File

@@ -28,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", 72);
private static final EventLogGroup LIFECYCLE = new EventLogGroup("lifecycle", 73);
private static final EventField<Boolean> eapField = EventFields.Boolean("eap");
private static final EventField<Boolean> testField = EventFields.Boolean("test");
@@ -66,14 +66,12 @@ public final class LifecycleUsageTriggerCollector extends CounterUsagesCollector
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 EventId2<PluginInfo, Long> IDE_FREEZE_DETECTED_PLUGIN =
LIFECYCLE.registerEvent("ide.freeze.detected.plugin", EventFields.PluginInfo, EventFields.DurationMs);
private static final EventId1<PluginInfo> IDE_FREEZE_PLUGIN_ISSUE_REPORTED =
LIFECYCLE.registerEvent("ide.freeze.reported.plugin", EventFields.PluginInfo);
private static final EventId1<PluginInfo> IDE_FREEZE_PLUGIN_IGNORED =
LIFECYCLE.registerEvent("ide.freeze.ignored.plugin", EventFields.PluginInfo);
private static final EventId IDE_FREEZE_PLUGIN_ISSUE_TRACKER_OPENED =
LIFECYCLE.registerEvent("ide.freeze.issue.tracker.opened");
private static final ClassEventField errorField = EventFields.Class("error");
private static final EventField<VMOptions.MemoryKind> memoryErrorKindField =
@@ -231,19 +229,15 @@ public final class LifecycleUsageTriggerCollector extends CounterUsagesCollector
EARLY_ERRORS.log(numErrors);
}
public static void pluginFreezeReported(@NotNull PluginId pluginId, long durationMs) {
IDE_FREEZE_REPORTED_PLUGIN.log(getPluginInfoById(pluginId), durationMs);
public static void pluginFreezeDetected(@NotNull PluginId pluginId, long durationMs) {
IDE_FREEZE_DETECTED_PLUGIN.log(getPluginInfoById(pluginId), durationMs);
}
public static void pluginDisabledOnFreeze(@NotNull PluginId id) {
IDE_FREEZE_PLUGIN_DISABLED.log(getPluginInfoById(id));
public static void pluginFreezeReported(@NotNull PluginId pluginId) {
IDE_FREEZE_PLUGIN_ISSUE_REPORTED.log(getPluginInfoById(pluginId));
}
public static void pluginFreezeIgnored(@NotNull PluginId id) {
IDE_FREEZE_PLUGIN_IGNORED.log(getPluginInfoById(id));
}
public static void pluginIssueTrackerOpened() {
IDE_FREEZE_PLUGIN_ISSUE_TRACKER_OPENED.log();
}
}

View File

@@ -4,6 +4,7 @@
<!-- only bundled plugin can define extension -->
<!--suppress PluginXmlDynamicPlugin -->
<extensionPoint name="registerToolWindowTaskProvider" interface="com.intellij.toolWindow.RegisterToolWindowTaskProvider"/>
<extensionPoint qualifiedName="com.intellij.diagnostic.freezeNotifier" interface="com.intellij.diagnostic.FreezeNotifier"/>
</extensionPoints>
<extensions defaultExtensionNs="com.intellij">