mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-18 09:34:34 +07:00
[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:
committed by
intellij-monorepo-bot
co-authored by
Denis Zaichenko
parent
3732a9d37a
commit
9f8e69b1f7
@@ -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 & 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
|
||||
|
||||
+11
-4
@@ -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
|
||||
+22
@@ -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))
|
||||
}
|
||||
}
|
||||
+56
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
+43
-19
@@ -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) {
|
||||
|
||||
+3
-2
@@ -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) {
|
||||
|
||||
-25
@@ -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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user