[diagnostics] IJPL-248422 Extended logging for freeze processing

[diagnostics] IJPL-248422 Extended logging for freeze processing

IJPL-249835 Propagate reporting settings to frontend

IJPL-249835 Make freeze reporting and error clustering configurable

[diagnostic] IJPL-248422 Do not deduplicate freeze reports in ExceptionAutoReportService

[diagnostic] IJPL-249978 Expand ErrorReportSink data with proper exception

[diagnostics] IJPL-248422 RD: host freezes are not auto-reported despite accepted consent

[diagnostics] IJPL-248422 RD: host freezes are not auto-reported despite accepted consent


Co-authored-by: Denis Zaichenko <denis.zaichenko@jetbrains.com>

Merge-request: IJ-MR-214253
Merged-by: Alesia Matuzok <alesia.matuzok@jetbrains.com>

Merge-request: IJ-MR-215173
Merged-by: Alesia Matuzok <alesia.matuzok@jetbrains.com>

GitOrigin-RevId: f7a108b55fd60067ad25c4a42a8101505141932f
This commit is contained in:
Yuriy Artamonov
2026-07-27 14:04:55 +00:00
committed by intellij-monorepo-bot
co-authored by Denis Zaichenko
parent 3732a9d37a
commit 9f8e69b1f7
16 changed files with 298 additions and 123 deletions
@@ -111,13 +111,11 @@ c:com.intellij.openapi.diagnostic.IdeaLoggingEvent
*:com.intellij.openapi.diagnostic.UnhandledErrorReport
*f:com.intellij.openapi.diagnostic.UnhandledExceptionReport
- com.intellij.openapi.diagnostic.UnhandledErrorReport
- <init>(java.lang.Class,java.util.List):V
- <init>(java.lang.Throwable):V
- f:getException():java.lang.Throwable
- f:getExceptionClass():java.lang.Class
- f:getStackTrace():java.util.List
*f:com.intellij.openapi.diagnostic.UnhandledFreezeReport
- com.intellij.openapi.diagnostic.UnhandledErrorReport
- <init>(java.lang.String,J,java.util.Collection,java.util.Collection):V
- f:getAttachments():java.util.Collection
- f:getDurationMs():J
- f:getMessage():java.lang.String
@@ -42,15 +42,22 @@ interface ErrorReportSink {
sealed interface UnhandledErrorReport
@ApiStatus.Experimental
class UnhandledExceptionReport(
class UnhandledExceptionReport @ApiStatus.Internal constructor(
/**
* @since 2026.2.1
*/
val exception: Throwable,
val exceptionClass: Class<*>,
@Deprecated("Use exception directly instead")
val stackTrace: List<StackTraceElement>,
) : UnhandledErrorReport {
constructor(t: Throwable) : this(t.javaClass, t.stackTrace.toList())
@ApiStatus.Internal
constructor(t: Throwable) : this(t, t.javaClass, t.stackTrace.toList())
}
@ApiStatus.Experimental
class UnhandledFreezeReport(
class UnhandledFreezeReport @ApiStatus.Internal constructor(
val message: String?,
val durationMs: Long,
val attachments: Collection<Attachment>,
@@ -12,9 +12,11 @@ import com.intellij.openapi.diagnostic.ProblematicPluginInfo
import com.intellij.openapi.diagnostic.ProblematicPluginInfoBasedOnDescriptor
import com.intellij.openapi.extensions.PluginId
import com.intellij.openapi.util.NlsSafe
import com.intellij.openapi.util.registry.Registry
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.async
import org.jetbrains.annotations.ApiStatus
@Service(Service.Level.APP)
internal class ErrorMessageClustering(private val coroutineScope: CoroutineScope) {
@@ -25,8 +27,9 @@ internal class ErrorMessageClustering(private val coroutineScope: CoroutineScope
internal fun clusterMessages(): Deferred<List<ErrorMessageCluster>> {
return coroutineScope.async {
val messages = MessagePool.getInstance().getFatalErrors(true, true)
val deduplicateReports = ErrorMessageClusteringSettings.isDeduplicationEnabled()
messages
.groupBy { hashMessage(it) }
.groupBy { if (deduplicateReports) hashMessage(it) else it }
.map { createCluster(it.value) }
}
}
@@ -39,7 +42,7 @@ internal class ErrorMessageClustering(private val coroutineScope: CoroutineScope
return ErrorMessageCluster(messages, pluginId, plugin, submitter)
}
private fun analyzeCause(first: AbstractMessage): PluginId? {
internal fun analyzeCause(first: AbstractMessage): PluginId? {
if (first.throwable.isInstance<Freeze>()) {
return IdeaFreezeReporter.analyzeFreeze(first)
}
@@ -58,6 +61,20 @@ internal class ErrorMessageClustering(private val coroutineScope: CoroutineScope
}
}
@ApiStatus.Internal
object ErrorMessageClusteringSettings {
const val DEDUPLICATE_REPORTS: String = "ide.errors.deduplicate"
@Volatile
private var deduplicationOverride: Boolean? = null
fun setDeduplicationOverride(enabled: Boolean?) {
deduplicationOverride = enabled
}
fun isDeduplicationEnabled(): Boolean = deduplicationOverride ?: Registry.`is`(DEDUPLICATE_REPORTS, true)
}
internal inline fun <reified T : Throwable> Throwable.isBackendInstance(): Boolean {
return this is RemoteSerializedThrowable && classFqn == T::class.qualifiedName
}
@@ -87,4 +104,4 @@ private class ProblematicPluginInfoBasedOnModel(val plugin: PluginUiModel) : Pro
get() = plugin.vendorDetails?.url
override val vendorEmail: String?
get() = null
}
}
@@ -5,7 +5,6 @@ import com.intellij.ide.AboutPopupDescriptionProvider
import com.intellij.ide.gdpr.Consent
import com.intellij.ide.gdpr.ConsentOptions
import com.intellij.ide.plugins.PluginManagerCore
import com.intellij.ide.plugins.PluginUtil
import com.intellij.ide.util.PropertiesComponent
import com.intellij.idea.AppMode
import com.intellij.openapi.application.ApplicationManager
@@ -33,9 +32,7 @@ object ExceptionAutoReportUtil {
// may be queried before Application started
val autoReportIsForbiddenForProduct: Boolean
get() = !ApplicationInfoImpl.getShadowInstance().isVendorJetBrains
|| AppMode.isRemoteDevHost() // we handle everything on client
|| AppMode.isHeadless()
get() = !ApplicationInfoImpl.getShadowInstance().isVendorJetBrains || AppMode.isHeadless()
suspend fun isAutoReportVisible(): Boolean {
return !autoReportIsForbiddenForProduct && RegistryManager.getInstanceAsync().`is`("ea.auto.report.feature.visible")
@@ -146,8 +143,10 @@ object ExceptionAutoReportUtil {
return null
}
val pluginId = PluginUtil.getInstance().findPluginId(throwable)
val pluginInfo = ErrorMessageClustering.getInstance().createPluginInfo(pluginId)
val errorMessageClustering = ErrorMessageClustering.getInstance()
val pluginId = errorMessageClustering.analyzeCause(message)
val pluginInfo = errorMessageClustering.createPluginInfo(pluginId)
val submitter = DefaultIdeaErrorLogger.findSubmitterByPluginInfo(throwable, pluginInfo)
val itnReporter = submitter as? ITNReporter ?: return null
@@ -210,3 +209,8 @@ internal class ReporterIdLoggerActivity : ProjectActivity {
internal enum class ForcedReportLevel {
ALL, FREEZES, NONE
}
@ApiStatus.Internal
interface ExceptionAutoReportService {
fun getResendAttempts(): Int
}
@@ -14,6 +14,7 @@ import com.intellij.openapi.application.impl.ApplicationImpl
import com.intellij.openapi.components.service
import com.intellij.openapi.components.serviceAsync
import com.intellij.openapi.diagnostic.Attachment
import com.intellij.openapi.diagnostic.fileLogger
import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.extensions.ExtensionNotApplicableException
import com.intellij.openapi.extensions.ExtensionPointName
@@ -22,7 +23,6 @@ import com.intellij.openapi.util.registry.Registry
import com.intellij.platform.eel.fs.EelFiles
import com.intellij.platform.ide.CoreUiCoroutineScopeHolder
import com.intellij.util.SmartList
import com.intellij.util.application
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.currentCoroutineContext
@@ -44,6 +44,8 @@ import kotlin.io.path.name
private val FREEZE_NOTIFIER_EP: ExtensionPointName<FreezeNotifier> = ExtensionPointName("com.intellij.diagnostic.freezeNotifier")
private val FREEZE_ANALYSIS_EP: ExtensionPointName<FreezeAnalysis> = ExtensionPointName("com.intellij.diagnostic.freezeAnalysis")
private val LOG = fileLogger()
internal class IdeaFreezeReporter : PerformanceListener {
private var dumpTask: IdeaFreezeSamplingTask? = null
private val currentDumps = Collections.synchronizedList(ArrayList<ThreadDump>())
@@ -86,14 +88,6 @@ internal class IdeaFreezeReporter : PerformanceListener {
}
}
internal fun report(event: LogMessage) {
// only report to JB
val plugin = PluginManagerCore.getPlugin(analyzeFreeze(event))
if (plugin == null || PluginManagerCore.isDevelopedByJetBrains(plugin)) {
MessagePool.getInstance().addErrorMessage(event)
}
}
internal fun analyzeFreeze(event: AbstractMessage): PluginId? {
for (attachment in event.allAttachments) {
if (attachment.name.startsWith(DUMP_PREFIX)) {
@@ -122,7 +116,7 @@ internal class IdeaFreezeReporter : PerformanceListener {
reset()
val maxDumpDuration = Registry.intValue("freeze.reporter.maxDumpDuration.ms", 40000)
val maxDumpDuration = FreezeReporterRegistry.maxDumpDurationMs()
if (maxDumpDuration <= 0) {
return
}
@@ -171,21 +165,38 @@ internal class IdeaFreezeReporter : PerformanceListener {
}
try {
if (Registry.`is`("freeze.reporter.enabled", false)) {
if (((durationMs / 1000).toInt() > FREEZE_THRESHOLD || ApplicationManagerEx.isInIntegrationTest()) && !stacktraceCommonPart.isNullOrEmpty()) {
val dumps = ArrayList(currentDumps) // defensive copy
if (dumpTask.isValid() && dumps.size >= 2) {
val attachments = ArrayList<Attachment>()
addDumpsAttachments(from = dumps, textMapper = { it.rawDump }, container = attachments)
if (reportDir != null) {
EP_NAME.forEachExtensionSafe { attachments.addAll(it.getAttachments(reportDir)) }
}
if (!FreezeReporterRegistry.isReporterEnabled()) {
return
}
val loggingEvent = createEvent(dumpTask, durationMs, attachments, reportDir, PerformanceWatcher.getInstance(), finished = true)
service<ITNProxyCoroutineScopeHolder>().coroutineScope.launch {
processDumps(dumps, reportDir, loggingEvent, durationMs)
}
}
LOG.debug("UI freeze recorded for $durationMs ms")
if ((durationMs / 1000).toInt() <= FreezeReporterRegistry.durationThresholdSeconds() && !ApplicationManagerEx.isInIntegrationTest()) {
LOG.debug("Ignoring freeze, below duration threshold")
return
}
if (stacktraceCommonPart.isNullOrEmpty()) {
LOG.debug("Ignoring freeze, no common stack found in dumps")
return
}
val dumps = ArrayList(currentDumps) // defensive copy
if (!dumpTask.isValid() || dumps.size < 2) {
LOG.debug("Ignoring freeze, not enough dumps collected")
return
}
val attachments = ArrayList<Attachment>()
addDumpsAttachments(from = dumps, textMapper = { it.rawDump }, container = attachments)
if (reportDir != null) {
EP_NAME.forEachExtensionSafe { attachments.addAll(it.getAttachments(reportDir)) }
}
val loggingEvent = createEvent(dumpTask, durationMs, attachments, reportDir, PerformanceWatcher.getInstance(), finished = true)
if (loggingEvent != null) {
service<ITNProxyCoroutineScopeHolder>().coroutineScope.launch {
processDumps(dumps, reportDir, loggingEvent, durationMs)
}
}
}
@@ -195,23 +206,28 @@ internal class IdeaFreezeReporter : PerformanceListener {
}
}
private suspend fun processDumps(dumps: ArrayList<ThreadDump>, reportDir: Path?, loggingEvent: LogMessage?, durationMs: Long) {
val autoReportEnabled = isAutoReportEnabledForFreezeReporter()
private suspend fun processDumps(dumps: ArrayList<ThreadDump>, reportDir: Path?, loggingEvent: LogMessage, durationMs: Long) {
if (dumps.isNotEmpty()) {
LOG.debug("Reporting freeze to MessagePool")
reportToIndicator(loggingEvent) // always put freezes to MessagePool
if (loggingEvent != null && (autoReportEnabled || application.isEAP || application.isInternal)) {
if (autoReportEnabled && ExceptionAutoReportUtil.isAutoReportableException(loggingEvent)) {
MessagePool.getInstance().addErrorMessage(loggingEvent)
return
}
else if (application.isEAP || application.isInternal) {
// plugin freezes are reported separately via com.intellij.diagnostic.FreezeNotifier
report(loggingEvent)
}
}
if (ExceptionAutoReportUtil.isAutoReportEnabled() && ExceptionAutoReportUtil.isAutoReportableException(loggingEvent)) {
LOG.debug("UI freeze will be automatically reported, do not show to user")
if (reportDir != null && loggingEvent != null && dumps.isNotEmpty()) {
for (notifier in FREEZE_NOTIFIER_EP.extensionList) {
notifier.notifyFreeze(loggingEvent, dumps, reportDir, durationMs)
val reason = analyzeFreeze(loggingEvent)
if (reason != null) {
LifecycleUsageTriggerCollector.pluginFreezeDetected(reason, durationMs, false)
}
return // do not show freeze notifications, reported automatically
}
if (reportDir != null) {
LOG.debug("Reporting freeze to plugin notifications")
for (notifier in FREEZE_NOTIFIER_EP.extensionList) {
notifier.notifyFreeze(loggingEvent, dumps, reportDir, durationMs)
}
}
}
}
@@ -294,6 +310,10 @@ ${if (finished) "" else if (appClosing) "IDE is closing. " else "IDE KILLED! "}S
}
}
internal fun reportToIndicator(event: LogMessage) {
MessagePool.getInstance().addErrorMessage(event)
}
@ApiStatus.Internal
object FreezeAnalysisFacade {
fun analyzeFreeze(dump: String): FreezeAnalysis.Result? {
@@ -386,9 +406,6 @@ private fun buildTree(threadInfos: List<ThreadInfo>, time: Int): CallTreeNode {
private val EP_NAME = ExtensionPointName<FreezeProfiler>("com.intellij.diagnostic.freezeProfiler")
// intentionally hardcoded and not implemented via a registry key or system property
// to be updated when we are ready to collect freezes from the specified duration and up
private const val FREEZE_THRESHOLD = 10
private const val REPORT_PREFIX = "report"
private const val DUMP_PREFIX = "dump"
private const val MESSAGE_FILE_NAME = ".message"
@@ -422,7 +439,7 @@ private suspend fun reportUnfinishedFreezes() {
}
// report deadly freeze
if (duration > FREEZE_THRESHOLD) {
if (duration > FreezeReporterRegistry.durationThresholdSeconds()) {
logger<IdeaFreezeReporter>().info("Detected unfinished freeze ${dir.name} with duration ${duration}ms")
try {
@@ -487,22 +504,17 @@ private suspend fun reportDeadlocks(files: List<Path>, duration: Int, dir: Path)
if (message != null && throwable != null && !attachments.isEmpty()) {
val event = LogMessage(throwable, message, attachments)
event.appInfo = appInfo
IdeaFreezeReporter.report(event)
reportToIndicator(event)
}
}
private suspend fun isUnfinishedFreezeReportEnabled(): Boolean {
val app = ApplicationManager.getApplication()
return app.isEAP || app.isInternal
|| isAutoReportEnabledForFreezeReporter()
|| ExceptionAutoReportUtil.isAutoReportEnabled()
|| System.getProperty("idea.force.freeze.reports").toBoolean()
}
private suspend fun isAutoReportEnabledForFreezeReporter(): Boolean {
return ExceptionAutoReportUtil.isAutoReportEnabled()
|| AppMode.isRemoteDevHost() && ExceptionAutoReportUtil.isAutoReportForced
}
private fun createReportAttachment(durationInSeconds: Long, text: String): Attachment =
Attachment("$REPORT_PREFIX-${durationInSeconds}s.txt", text).apply { this.isIncluded = true }
@@ -629,3 +641,44 @@ private class IdeaFreezeSamplingTask(val reportDir: Path, maxDurationMs: Int, co
fun isValid(): Boolean = sampleCount > (1000 / dumpInterval)
}
@ApiStatus.Internal
object FreezeReporterRegistry {
const val ENABLED: String = "freeze.reporter.enabled"
const val MAX_DUMP_DURATION_MS: String = "freeze.reporter.maxDumpDuration.ms"
const val DURATION_THRESHOLD_SECONDS: String = "freeze.reporter.duration.threshold.seconds"
private const val DEFAULT_MAX_DUMP_DURATION_MS = 40_000
private const val DEFAULT_DURATION_THRESHOLD_SECONDS = 10
@Volatile
private var overrides = FreezeReporterOverrides()
fun setOverrides(
enabled: Boolean?,
maxDumpDurationMs: Int?,
durationThresholdSeconds: Int?,
) {
overrides = FreezeReporterOverrides(enabled, maxDumpDurationMs, durationThresholdSeconds)
}
fun isReporterEnabled(): Boolean = overrides.enabled ?: Registry.`is`(ENABLED, false)
fun maxDumpDurationMs(): Int {
return overrides.maxDumpDurationMs ?: Registry.intValue(MAX_DUMP_DURATION_MS, DEFAULT_MAX_DUMP_DURATION_MS)
}
fun durationThresholdSeconds(): Int {
val threshold = overrides.durationThresholdSeconds ?: Registry.intValue(
DURATION_THRESHOLD_SECONDS,
DEFAULT_DURATION_THRESHOLD_SECONDS,
)
return threshold.coerceAtLeast(0)
}
}
private data class FreezeReporterOverrides(
val enabled: Boolean? = null,
val maxDumpDurationMs: Int? = null,
val durationThresholdSeconds: Int? = null,
)
@@ -771,7 +771,7 @@ private suspend fun reportCrashesIfAny() {
val event = LogMessage(JBRCrash(), message, attachments)
event.appInfo = Files.readString(appInfoFile)
IdeaFreezeReporter.report(event)
reportToIndicator(event)
LifecycleUsageTriggerCollector.onCrashDetected()
}
}
@@ -97,24 +97,30 @@ public final class IdeaLogger extends JulLogger {
return StringUtil.shortenTextWithEllipsis(message, 300, 0);
}
private static void reportToFus(Throwable t) {
private static void reportToFus(Throwable rawThrowable) {
if (!LoadingState.COMPONENTS_LOADED.isOccurred() || FUS_RECURSION_GUARD.get() != null) {
return;
}
FUS_RECURSION_GUARD.set(true);
var throwable = rawThrowable;
if (rawThrowable instanceof UnhandledException uh) {
throwable = uh.getCause();
}
try {
var app = ApplicationManager.getApplication();
if (app != null && !app.isUnitTestMode() && !app.isDisposed()) {
var pluginUtil = PluginUtil.getInstance();
if (pluginUtil != null) {
var pluginId = pluginUtil.findPluginId(t);
var kind = DefaultIdeaErrorLogger.getOOMErrorKind(t);
LifecycleUsageTriggerCollector.onError(pluginId, t, kind);
var pluginId = pluginUtil.findPluginId(throwable);
var kind = DefaultIdeaErrorLogger.getOOMErrorKind(throwable);
LifecycleUsageTriggerCollector.onError(pluginId, throwable, kind);
if (pluginId != null) {
var sinkService = UnhandledReportSinkService.getInstance();
if (sinkService != null) { // might be null in CLI utils
sinkService.report(new PluginExceptionReportData(pluginId, t));
sinkService.report(new PluginExceptionReportData(pluginId, throwable));
}
}
}
@@ -1905,6 +1905,10 @@ The refresh itself is toggled by (Preferences | Appearance &amp; Behavior | Syst
description="Report UI freezes to JetBrains"/>
<registryKey key="freeze.reporter.maxDumpDuration.ms" defaultValue="40000"
description="Maximum duration of the 'report.txt' performance snapshot in ms"/>
<registryKey key="freeze.reporter.duration.threshold.seconds" defaultValue="10"
description="Report UI freezes longer than the specified duration in seconds"/>
<registryKey key="ide.errors.deduplicate" defaultValue="true"
description="Group error reports with identical stack traces in the error dialog"/>
<registryKey key="freeze.reporter.profiling" defaultValue="true"
description="Start CPU profiling when the IDE is frozen"/>
<registryKey key="freeze.reporter.profiling.all.threads" defaultValue="true"
@@ -83,8 +83,6 @@ auto.report.enabled.title=Thank you for your assistance!
auto.report.enabled.text=New errors will be reported automatically.
auto.report.enabled.settings.action=Open settings
action.ResetFreezeNotificationState.text=Reset Freezes Notification State
event.watcher.tab.title.invocations=Invocations
event.watcher.tab.title.runnables=Runnables
event.watcher.tab.title.wrappers=Wrappers
@@ -20,11 +20,18 @@
<applicationSettings service="com.intellij.performanceTesting.freezes.PluginsFreezesService"/>
<backgroundPostStartupActivity implementation="com.intellij.performanceTesting.freezes.promo.ErrorReportPromoterActivity"/>
<diagnostic.freezeNotifier implementation="com.intellij.performanceTesting.freezes.promo.FreezeCounterListener"/>
<statistics.applicationUsagesCollector implementation="com.intellij.performanceTesting.freezes.AutoReportExceptionsStateCollector"/>
</extensions>
<actions resource-bundle="messages.PerformanceTestingBundle">
<action id="ResetFreezeNotificationState" class="com.intellij.performanceTesting.freezes.ResetFreezeNotificationStateAction"
internal="true"/>
<actions resource-bundle="messages.PluginFreezeBundle">
<group internal="true" id="Internal.Errors.Additional">
<separator/>
<action id="ResetFreezeNotificationState" class="com.intellij.performanceTesting.freezes.ResetFreezeNotificationStateAction"
internal="true"/>
<action id="PrintErrorAutoReporterState" class="com.intellij.performanceTesting.freezes.ErrorAutoReporterStateInternalAction"
internal="true">
</action>
<add-to-group group-id="Internal.Errors" anchor="after" relative-to-action="DebugListen"/>
</group>
</actions>
</idea-plugin>
@@ -5,3 +5,6 @@ notification.content.plugin.caused.freeze=Plugin ''{0}'' might be slowing things
action.report.text=Report problem
action.ignore.plugin.tooltip=Ignore such performance issues in the future
action.dismiss.tooltip=Dismiss the notification
action.ResetFreezeNotificationState.text=Reset Freezes Notification State
action.PrintErrorAutoReporterState.text=State of Sending Error Reports
@@ -0,0 +1,22 @@
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.performanceTesting.freezes
import com.intellij.diagnostic.ExceptionAutoReportUtil
import com.intellij.internal.statistic.beans.MetricEvent
import com.intellij.internal.statistic.eventLog.EventLogGroup
import com.intellij.internal.statistic.eventLog.events.EventFields
import com.intellij.internal.statistic.service.fus.collectors.ApplicationUsagesCollector
internal class AutoReportExceptionsStateCollector : ApplicationUsagesCollector() {
private val GROUP = EventLogGroup("exceptions.auto.report", 2)
override fun getGroup(): EventLogGroup = GROUP
private val FIELD_ENABLED = EventFields.Boolean("enabled")
private val STATE_EVENT = GROUP.registerEvent("state.of.reporting", FIELD_ENABLED)
override suspend fun getMetricsAsync(): Set<MetricEvent> {
val enabled = ExceptionAutoReportUtil.isAutoReportEnabled()
return setOf(STATE_EVENT.metric(enabled))
}
}
@@ -0,0 +1,56 @@
package com.intellij.performanceTesting.freezes
import com.intellij.diagnostic.ExceptionAutoReportService
import com.intellij.diagnostic.ExceptionAutoReportUtil
import com.intellij.openapi.actionSystem.ActionUpdateThread
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.application.EDT
import com.intellij.openapi.components.serviceOrNull
import com.intellij.openapi.fileEditor.FileEditorManager
import com.intellij.openapi.fileTypes.PlainTextFileType
import com.intellij.testFramework.LightVirtualFile
import com.intellij.util.application
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
internal class ErrorAutoReporterStateInternalAction : AnAction() {
override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.BGT
override fun update(e: AnActionEvent) {
e.presentation.isVisible = application.isInternal && e.project != null
e.presentation.isEnabled = true
}
override fun actionPerformed(e: AnActionEvent) {
val project = e.project ?: return
e.coroutineScope.launch {
val message = computeMessage()
withContext(Dispatchers.EDT) {
val file = LightVirtualFile("Error reporting status", PlainTextFileType.INSTANCE, message)
FileEditorManager.getInstance(project).openFile(file, true, true)
}
}
}
private suspend fun computeMessage(): String {
if (ExceptionAutoReportUtil.autoReportIsForbiddenForProduct) {
return "Auto report is forbidden for this product"
}
if (!ExceptionAutoReportUtil.isAutoReportVisible()) {
return "Auto report feature is invisible"
}
if (!ExceptionAutoReportUtil.isAutoReportEnabled()) {
return "Auto report is disabled"
}
val resendAttempts = serviceOrNull<ExceptionAutoReportService>()?.getResendAttempts() ?: -1
return when (resendAttempts) {
-1 -> "No attempts to send exceptions have been made by now"
0 -> "The latest batch of exceptions was successfully sent"
else -> "$resendAttempts attempts to send made"
}
}
}
@@ -10,6 +10,7 @@ import com.intellij.featureStatistics.fusCollectors.LifecycleUsageTriggerCollect
import com.intellij.ide.plugins.PluginManagerCore
import com.intellij.ide.plugins.PluginManagerCore.isVendorJetBrains
import com.intellij.ide.setToolTipText
import com.intellij.ide.util.PropertiesComponent
import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.application.impl.ApplicationInfoImpl
import com.intellij.openapi.diagnostic.UnhandledReportSinkService
@@ -23,6 +24,8 @@ import com.intellij.openapi.project.ProjectManager
import com.intellij.openapi.util.registry.Registry
import com.intellij.openapi.util.text.HtmlChunk
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.performanceTesting.freezes.promo.FREEZE_COUNT_KEY
import com.intellij.performanceTesting.freezes.promo.FREEZE_THRESHOLD
import com.intellij.ui.EditorNotificationPanel
import com.intellij.ui.EditorNotificationProvider
import com.intellij.ui.EditorNotifications
@@ -39,13 +42,15 @@ internal class PluginFreezeNotifier : FreezeNotifier {
val freezeReason = freezeWatcher.getFreezeReason()
if (freezeReason != null) return // still have previous reason shown to user
countFreezes()
for (dump in currentDumps) {
val reason = freezeWatcher.dumpedThreads(event, dump, durationMs)
if (reason != null) {
LifecycleUsageTriggerCollector.pluginFreezeDetected(reason.pluginId, durationMs, reason.reportToUser)
thisLogger().warn("Identified UI freeze in plugin ${reason.pluginId} for $durationMs ms")
if (reason.reportToUser) {
reportFreeze()
updateUi()
}
UnhandledReportSinkService.getInstance()?.report(PluginFreezeReportData(
@@ -60,11 +65,23 @@ internal class PluginFreezeNotifier : FreezeNotifier {
}
}
private fun reportFreeze() {
private fun updateUi() {
for (project in ProjectManager.getInstance().openProjects) {
EditorNotifications.getInstance(project).updateAllNotifications()
}
}
internal fun countFreezes() {
val props = PropertiesComponent.getInstance()
val currentCount = props.getInt(FREEZE_COUNT_KEY, 0)
if (currentCount > FREEZE_THRESHOLD) {
thisLogger().debug("Freeze count exceeded threshold, do not count further")
return
}
props.setValue(FREEZE_COUNT_KEY, currentCount + 1, 0)
thisLogger().debug("Freeze detected, incrementing freeze count for promo")
}
}
internal class PluginFreezeNotificationPanel : EditorNotificationProvider {
@@ -114,25 +131,32 @@ internal class PluginFreezeNotificationPanel : EditorNotificationProvider {
if (reported.add(freezeReason)) {
// must be added only once
MessagePool.getInstance().addErrorMessage(freezeReason.event).invokeOnCompletion {
application.invokeLater(
{
if (project.isDisposed) return@invokeLater
val dialog = object : IdeErrorsDialog(MessagePool.getInstance(), project, ijProject, freezeReason.event) {
override fun updateOnSubmit() {
super.updateOnSubmit()
PluginsFreezesService.getInstance().mutePlugin(pluginDescriptor.pluginId)
LifecycleUsageTriggerCollector.pluginFreezeReported(pluginDescriptor.pluginId)
closePanel(project)
}
}
dialog.show()
}, ModalityState.nonModal())
openInErrorDialog(project, ijProject, freezeReason, pluginDescriptor)
}
}
else { // already added to pool
openInErrorDialog(project, ijProject, freezeReason, pluginDescriptor)
}
}
private fun openInErrorDialog(project: Project, ijProject: Boolean, freezeReason: FreezeReason, pluginDescriptor: PluginDescriptor) {
application.invokeLater(
{
if (project.isDisposed) return@invokeLater
val dialog = object : IdeErrorsDialog(MessagePool.getInstance(), project, ijProject, freezeReason.event) {
override fun updateOnSubmit() {
super.updateOnSubmit()
PluginsFreezesService.getInstance().mutePlugin(pluginDescriptor.pluginId)
LifecycleUsageTriggerCollector.pluginFreezeReported(pluginDescriptor.pluginId)
closePanel(project)
}
}
dialog.show()
}, ModalityState.nonModal())
}
private fun closePanel(project: Project) {
@@ -17,7 +17,6 @@ import com.intellij.openapi.extensions.ExtensionNotApplicableException
import com.intellij.openapi.project.Project
import com.intellij.openapi.startup.ProjectActivity
import com.intellij.openapi.util.registry.RegistryManager
import com.intellij.platform.ide.productMode.IdeProductMode
import com.intellij.ui.AppUIUtil
import com.intellij.util.Time
import com.intellij.util.application
@@ -28,11 +27,13 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
private const val PROMO_SHOWN_KEY = "promo.notification.automatic.error.report.shown"
internal const val FREEZE_COUNT_KEY = "performance.plugin.promo.freeze.count"
internal const val FREEZE_THRESHOLD = 3
internal class ErrorReportPromoterActivity : ProjectActivity {
init {
if (application.isHeadlessEnvironment() || IdeProductMode.isBackend) throw ExtensionNotApplicableException.create()
if (application.isHeadlessEnvironment) throw ExtensionNotApplicableException.create()
}
override suspend fun execute(project: Project) {
@@ -1,25 +0,0 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.performanceTesting.freezes.promo
import com.intellij.diagnostic.FreezeNotifier
import com.intellij.diagnostic.LogMessage
import com.intellij.diagnostic.ThreadDump
import com.intellij.ide.util.PropertiesComponent
import com.intellij.openapi.diagnostic.thisLogger
import java.nio.file.Path
internal const val FREEZE_COUNT_KEY = "performance.plugin.promo.freeze.count"
internal class FreezeCounterListener : FreezeNotifier {
override fun notifyFreeze(event: LogMessage, currentDumps: Collection<ThreadDump>, reportDir: Path, durationMs: Long) {
val props = PropertiesComponent.getInstance()
val currentCount = props.getInt(FREEZE_COUNT_KEY, 0)
if (currentCount > FREEZE_THRESHOLD) {
thisLogger().debug("Freeze count exceeded threshold, do not count further")
return
}
props.setValue(FREEZE_COUNT_KEY, currentCount + 1, 0)
thisLogger().debug("Freeze detected, incrementing freeze count for promo")
}
}