IJPL-253712 improved ProgressManager diagnostics

(cherry picked from commit dc41a034b2f8142f4743d1d01a689aafe757483b)
(IJ-CR-220046 IJPL-253712 improved ProgressManager diagnostics)

GitOrigin-RevId: 88012734da3d286fe61bd226c03da9f5c5fdee7b
This commit is contained in:
Alexey Kudravtsev
2026-08-25 16:47:31 +00:00
committed by intellij-monorepo-bot
parent f6b69f2728
commit ecd362eccd
4 changed files with 114 additions and 26 deletions
@@ -299,6 +299,9 @@ interface ThreadingSupport {
return NAME
}
}
@ApiStatus.Internal
fun dumpSomeDiagnosticInfo(thread: Thread): List<String>
}
typealias CleanupAction = () -> Unit
@@ -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<ProgressIndicator, Set<Thread>> threadsUnderIndicator = new HashMap<>(); // guarded by threadsUnderIndicator
// the active indicator for the thread id
private static final ConcurrentLongObjectMap<ProgressIndicator> currentIndicators =
Java11Shim.Companion.createConcurrentLongObjectMap();
private static final ConcurrentLongObjectMap<ProgressIndicator> currentIndicators = Java11Shim.Companion.createConcurrentLongObjectMap();
// top-level indicators for the thread id
private static final ConcurrentLongObjectMap<ProgressIndicator> threadTopLevelIndicators =
Java11Shim.Companion.createConcurrentLongObjectMap();
private static final ConcurrentLongObjectMap<ProgressIndicator> threadTopLevelIndicators = Java11Shim.Companion.createConcurrentLongObjectMap();
// threads which are running under canceled indicator
private static final Set<Thread> threadsUnderCanceledIndicator = new HashSet<>(); // guarded by threadsUnderIndicator
@@ -132,7 +137,7 @@ public class CoreProgressManager extends ProgressManager implements Disposable {
private static final Map<ProgressIndicator, AtomicInteger> 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<Thread, ProgressIndicator> threadIndicators = new MultiMap<>();
for (Map.Entry<ProgressIndicator, Set<Thread>> entry : threadsUnderIndicator.entrySet()) {
ProgressIndicator indicator = entry.getKey();
Set<Thread> 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<Long, ThreadInfo> 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<Thread, Collection<ProgressIndicator>> entry : threadIndicators.toHashMap().entrySet()) {
Thread thread = entry.getKey();
Collection<ProgressIndicator> indicators = entry.getValue();
long threadId = thread.getId();
ProgressIndicator current = currentIndicators.get(threadId);
ProgressIndicator topLevel = threadTopLevelIndicators.get(threadId);
List<String> 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;
}
}
@@ -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<String> {
val r = mutableListOf<String>()
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
}
}
@@ -12,7 +12,7 @@ object ProgressIndicatorDumper {
private const val PROGRESS_INDICATOR_DUMP_HEADER: @NonNls String = "---------- ProgressIndicator dump ----------"
@Volatile
private var PROGRESS_INDICATOR_DUMPER: Supplier<String?>? = null
private var PROGRESS_INDICATOR_DUMPER: Supplier<String>? = 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<String?>?) {
fun setProgressIndicatorDumper(dumpProvider: Supplier<String>?) {
PROGRESS_INDICATOR_DUMPER = dumpProvider
}