From ecd362eccd9e0c787a3e66dd8e92d132eee9aa3f Mon Sep 17 00:00:00 2001 From: Alexey Kudravtsev Date: Mon, 24 Aug 2026 17:57:56 +0200 Subject: [PATCH] IJPL-253712 improved ProgressManager diagnostics (cherry picked from commit dc41a034b2f8142f4743d1d01a689aafe757483b) (IJ-CR-220046 IJPL-253712 improved ProgressManager diagnostics) GitOrigin-RevId: 88012734da3d286fe61bd226c03da9f5c5fdee7b --- .../openapi/application/ThreadingSupport.kt | 3 + .../progress/impl/CoreProgressManager.java | 89 ++++++++++++++----- .../src/NestedLocksThreadingSupport.kt | 44 +++++++++ .../diagnostic/progressIndicatorDumper.kt | 4 +- 4 files changed, 114 insertions(+), 26 deletions(-) diff --git a/platform/core-api/src/com/intellij/openapi/application/ThreadingSupport.kt b/platform/core-api/src/com/intellij/openapi/application/ThreadingSupport.kt index 9087b970110b..aa5f39fb9871 100644 --- a/platform/core-api/src/com/intellij/openapi/application/ThreadingSupport.kt +++ b/platform/core-api/src/com/intellij/openapi/application/ThreadingSupport.kt @@ -299,6 +299,9 @@ interface ThreadingSupport { return NAME } } + + @ApiStatus.Internal + fun dumpSomeDiagnosticInfo(thread: Thread): List } typealias CleanupAction = () -> Unit \ No newline at end of file diff --git a/platform/core-impl/src/com/intellij/openapi/progress/impl/CoreProgressManager.java b/platform/core-impl/src/com/intellij/openapi/progress/impl/CoreProgressManager.java index 0228ae7a0bc7..e57ce93789ae 100644 --- a/platform/core-impl/src/com/intellij/openapi/progress/impl/CoreProgressManager.java +++ b/platform/core-impl/src/com/intellij/openapi/progress/impl/CoreProgressManager.java @@ -9,6 +9,7 @@ import com.intellij.openapi.Disposable; import com.intellij.openapi.application.Application; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.application.ModalityState; +import com.intellij.openapi.application.ThreadingSupport; import com.intellij.openapi.application.WriteIntentReadAction; import com.intellij.openapi.application.ex.ApplicationEx; import com.intellij.openapi.application.ex.ApplicationManagerEx; @@ -41,6 +42,7 @@ import com.intellij.util.SystemProperties; import com.intellij.util.concurrency.AppExecutorUtil; import com.intellij.util.containers.ConcurrentLongObjectMap; import com.intellij.util.containers.Java11Shim; +import com.intellij.util.containers.MultiMap; import com.intellij.util.ui.EDT; import io.opentelemetry.api.trace.Span; import kotlinx.coroutines.Job; @@ -53,8 +55,12 @@ import org.jetbrains.annotations.VisibleForTesting; import javax.swing.JComponent; import java.io.StringWriter; +import java.lang.management.LockInfo; +import java.lang.management.ThreadInfo; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -71,6 +77,7 @@ import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.locks.LockSupport; import java.util.function.Supplier; +import java.util.stream.Collectors; import static com.intellij.openapi.application.ModalityKt.currentThreadContextModality; import static com.intellij.openapi.progress.impl.ProgressManagerScopeKt.ProgressManagerScope; @@ -89,11 +96,9 @@ public class CoreProgressManager extends ProgressManager implements Disposable { // THashMap is avoided here because of tombstones overhead private static final Map> threadsUnderIndicator = new HashMap<>(); // guarded by threadsUnderIndicator // the active indicator for the thread id - private static final ConcurrentLongObjectMap currentIndicators = - Java11Shim.Companion.createConcurrentLongObjectMap(); + private static final ConcurrentLongObjectMap currentIndicators = Java11Shim.Companion.createConcurrentLongObjectMap(); // top-level indicators for the thread id - private static final ConcurrentLongObjectMap threadTopLevelIndicators = - Java11Shim.Companion.createConcurrentLongObjectMap(); + private static final ConcurrentLongObjectMap threadTopLevelIndicators = Java11Shim.Companion.createConcurrentLongObjectMap(); // threads which are running under canceled indicator private static final Set threadsUnderCanceledIndicator = new HashSet<>(); // guarded by threadsUnderIndicator @@ -132,7 +137,7 @@ public class CoreProgressManager extends ProgressManager implements Disposable { private static final Map nonStandardIndicators = new ConcurrentHashMap<>(); public CoreProgressManager() { - ProgressIndicatorDumper.INSTANCE.setProgressIndicatorDumper(this::getProgressStateRepresentation); + ProgressIndicatorDumper.INSTANCE.setProgressIndicatorDumper(() -> getProgressStateRepresentation()); } // must be under threadsUnderIndicator lock @@ -1097,7 +1102,7 @@ public class CoreProgressManager extends ProgressManager implements Disposable { return contextModality; } - ProgressManager progressManager = ProgressManager.getInstanceOrNull(); + ProgressManager progressManager = getInstanceOrNull(); ModalityState progressModality = progressManager == null ? null : progressManager.getCurrentProgressModality(); return progressModality == null ? ModalityState.nonModal() : progressModality; } @@ -1180,32 +1185,68 @@ public class CoreProgressManager extends ProgressManager implements Disposable { /** * A utility method for diagnosing state of progress indicator in monitoring facilities, like JStack */ - @ApiStatus.Internal - public @Nullable String getProgressStateRepresentation() { + private static @NotNull String getProgressStateRepresentation() { synchronized (threadsUnderIndicator) { - StringBuilder result = new StringBuilder(); - if (threadsUnderIndicator.isEmpty()) { - return null; - } + int totalIndicators = threadsUnderIndicator.size(); + String result = totalIndicators+" indicators registered:\n"; + MultiMap threadIndicators = new MultiMap<>(); for (Map.Entry> entry : threadsUnderIndicator.entrySet()) { ProgressIndicator indicator = entry.getKey(); Set threads = entry.getValue(); - result.append("Indicator ").append(renderProgressIndicator(indicator)).append(" corresponds to the following threads:\n"); for (Thread thread : threads) { - result.append(" - ").append(thread).append(";\n"); + threadIndicators.putValue(thread, indicator); } } - return result.toString(); + Map threadInfos = Arrays.stream(ThreadDumper.getThreadInfos()).collect(Collectors.toMap(info -> info.getThreadId(), info -> info)); + ThreadingSupport threadingSupport = ApplicationManager.getApplication().getThreadingSupport(); + boolean writeActionPending = threadingSupport != null && threadingSupport.isWriteActionPending(); + boolean writeActionInProgress = threadingSupport != null && threadingSupport.isWriteActionInProgress(); + for (Map.Entry> entry : threadIndicators.toHashMap().entrySet()) { + Thread thread = entry.getKey(); + Collection indicators = entry.getValue(); + long threadId = thread.getId(); + ProgressIndicator current = currentIndicators.get(threadId); + ProgressIndicator topLevel = threadTopLevelIndicators.get(threadId); + List readActionStatus = threadingSupport == null ? Collections.emptyList() : threadingSupport.dumpSomeDiagnosticInfo(thread); + result += readableThreadInfo(threadInfos.get(threadId)) + "\n" + + (readActionStatus.isEmpty() && !writeActionPending && !writeActionInProgress ? "" : + " rw action status:" + readActionStatus + (writeActionPending || writeActionInProgress ? "(writeActionPending:"+writeActionPending+", writeActionInProgress:"+writeActionInProgress+")" : "") + "\n") + + (current == null ? "" : + " current indicator: " + current+"\n") + + (current == topLevel ? "" : + " top level indicator: " + topLevel + "\n") + + (indicators.isEmpty() ? "" : + " owns " + indicators.size() + " indicators:"+"\n"); + for (ProgressIndicator indicator : indicators) { + result += + " " + indicator + "("+indicator.getClass()+" canceled: " + indicator.isCanceled() + ", running:" + indicator.isRunning() + ")" + "\n"; + } + } + return result; } } - - private static String renderProgressIndicator(ProgressIndicator indicator) { - String presentationBuilder = indicator.toString() + - " (canceled: " + - indicator.isCanceled() + - ", running:" + - indicator.isRunning() + - ")"; - return presentationBuilder; + private static String readableThreadInfo(@Nullable ThreadInfo info) { + if (info == null) return ""; + String sb = info.getThreadName() + " Id=" + info.getThreadId() + " " + info.getThreadState(); + if (info.getLockName() != null) { + sb += " on " + info.getLockName(); + } + if (info.getLockOwnerName() != null) { + sb += " owned by \"" + info.getLockOwnerName() + "\" Id=" + info.getLockOwnerId(); + } + if (info.isSuspended()) { + sb += " (suspended)"; + } + if (info.isInNative()) { + sb += " (in native)"; + } + LockInfo[] locks = info.getLockedSynchronizers(); + if (locks.length > 0) { + sb += "\n\tNumber of locked synchronizers = " + locks.length + '\n'; + for (LockInfo li : locks) { + sb += "\t- " + li + '\n'; + } + } + return sb; } } diff --git a/platform/locking-impl/src/NestedLocksThreadingSupport.kt b/platform/locking-impl/src/NestedLocksThreadingSupport.kt index 04caee0d0178..b76f65c8d196 100644 --- a/platform/locking-impl/src/NestedLocksThreadingSupport.kt +++ b/platform/locking-impl/src/NestedLocksThreadingSupport.kt @@ -37,6 +37,7 @@ import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.Runnable import kotlinx.coroutines.withContext import org.jetbrains.annotations.ApiStatus +import java.lang.ref.WeakReference import java.util.Arrays import java.util.Collections import java.util.concurrent.CopyOnWriteArrayList @@ -1681,6 +1682,22 @@ class NestedLocksThreadingSupport : ThreadingSupport { "The executor must run the action synchronously" } } + + override fun dumpSomeDiagnosticInfo(thread: Thread): List { + val r = mutableListOf() + val readActionsInThread = pokeThreadLocalValueWithStick(thread, myReadActionsInThread) + if (readActionsInThread != null && readActionsInThread.toString() != "0") { + r += "(nested read actions: $readActionsInThread)" + } + val topMostReadAction = pokeThreadLocalValueWithStick(thread, myTopmostReadAction) + if (topMostReadAction != null && topMostReadAction.toString() != "false") { + r += "(in top-most read action)" + } + if (myWriteAcquired == thread) { + r += "(write action acquired)" + } + return r + } } @@ -1770,3 +1787,30 @@ private data class PermitWaitingInterceptor( val consumer: (Deferred<*>) -> Unit, ) +private fun pokeThreadLocalValueWithStick(targetThread: Thread, targetThreadLocal: ThreadLocal<*>): Any? { + try { + // 1. Get the 'threadLocals' field from the target Thread object + val threadLocalsField = Thread::class.java.getDeclaredField("threadLocals") + threadLocalsField.setAccessible(true) + val threadLocalMap = threadLocalsField.get(targetThread) + if (threadLocalMap == null) { + return null // Map hasn't been initialized yet + } + // 2. Locate the 'getEntry' method inside ThreadLocalMap + val getEntryMethod = Class.forName($$"java.lang.ThreadLocal$ThreadLocalMap").getDeclaredMethod("getEntry", ThreadLocal::class.java) + getEntryMethod.setAccessible(true) + // 3. Invoke 'getEntry' to extract the map entry for your ThreadLocal key + val entry = getEntryMethod.invoke(threadLocalMap, targetThreadLocal) as WeakReference<*>? + if (entry == null) { + return null + } + // 4. Extract the 'value' field from that Entry + val valueField = Class.forName($$"java.lang.ThreadLocal$ThreadLocalMap$Entry").getDeclaredField("value") + valueField.setAccessible(true) + return valueField.get(entry) + } + catch (e: Exception) { + e.printStackTrace() + return null + } +} diff --git a/platform/util/base/src/com/intellij/diagnostic/progressIndicatorDumper.kt b/platform/util/base/src/com/intellij/diagnostic/progressIndicatorDumper.kt index 2c0ff7543372..a8055b91b893 100644 --- a/platform/util/base/src/com/intellij/diagnostic/progressIndicatorDumper.kt +++ b/platform/util/base/src/com/intellij/diagnostic/progressIndicatorDumper.kt @@ -12,7 +12,7 @@ object ProgressIndicatorDumper { private const val PROGRESS_INDICATOR_DUMP_HEADER: @NonNls String = "---------- ProgressIndicator dump ----------" @Volatile - private var PROGRESS_INDICATOR_DUMPER: Supplier? = null + private var PROGRESS_INDICATOR_DUMPER: Supplier? = null fun dumpProgressIndicatorState(): String { return (PROGRESS_INDICATOR_DUMPER?.get() ?: "No progress indicator dump") @@ -26,7 +26,7 @@ object ProgressIndicatorDumper { return PROGRESS_INDICATOR_DUMP_HEADER + "\n" + this } - fun setProgressIndicatorDumper(dumpProvider: Supplier?) { + fun setProgressIndicatorDumper(dumpProvider: Supplier?) { PROGRESS_INDICATOR_DUMPER = dumpProvider }