diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/Target_java_lang_management_ThreadInfo.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/Target_java_lang_management_ThreadInfo.java new file mode 100644 index 000000000000..2f04c9bf78e2 --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/Target_java_lang_management_ThreadInfo.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2023, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.core.jdk; + +import com.oracle.svm.core.annotate.Alias; +import com.oracle.svm.core.annotate.TargetClass; + +@SuppressWarnings({"unused"}) +@TargetClass(java.lang.management.ThreadInfo.class) +final class Target_java_lang_management_ThreadInfo { + + @Alias + Target_java_lang_management_ThreadInfo(Thread t, int state, Object lockObj, Thread lockOwner, + long blockedCount, long blockedTime, + long waitedCount, long waitedTime, + StackTraceElement[] stackTrace, + Object[] monitors, + int[] stackDepths, + Object[] synchronizers) { + } +} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/ThreadMXUtils.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/ThreadMXUtils.java new file mode 100644 index 000000000000..509ec4e08e7b --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/ThreadMXUtils.java @@ -0,0 +1,158 @@ +/* + * Copyright (c) 2023, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.core.jdk; + +import com.oracle.svm.core.SubstrateUtil; +import com.oracle.svm.core.jdk.management.SubstrateThreadMXBean; +import com.oracle.svm.core.locks.Target_java_util_concurrent_locks_AbstractOwnableSynchronizer; +import com.oracle.svm.core.monitor.JavaMonitor; +import com.oracle.svm.core.thread.JavaThreads.JMXMonitoring; +import com.oracle.svm.core.thread.PlatformThreads; + +import java.lang.management.ThreadInfo; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.concurrent.locks.AbstractOwnableSynchronizer; +import java.util.concurrent.locks.LockSupport; + +/** + * Utils to support {@link SubstrateThreadMXBean} for providing threading information for JMX + * support. Include the {@link ThreadInfo} constructing utils, and deadlock detection utils. + */ +public class ThreadMXUtils { + + public static class ThreadInfoConstructionUtils { + + private static StackTraceElement[] getStackTrace(Thread thread, int maxDepth) { + StackTraceElement[] stackTrace = thread.getStackTrace(); + return maxDepth == -1 || maxDepth >= stackTrace.length ? stackTrace : Arrays.copyOfRange(stackTrace, 0, maxDepth); + } + + private record Blocker(Object blockObject, Thread ownerThread) { + } + + private static Blocker getBlockerInfo(Thread thread) { + Object blocker = LockSupport.getBlocker(thread); + + if (blocker instanceof JavaMonitor javaMonitor) { + return new Blocker( + javaMonitor.getBlockedObject(), + SubstrateThreadMXBean.getThreadById(javaMonitor.getOwnerThreadId())); + } else if (blocker instanceof AbstractOwnableSynchronizer synchronizer) { + return new Blocker(synchronizer, + SubstrateUtil.cast(synchronizer, Target_java_util_concurrent_locks_AbstractOwnableSynchronizer.class) + .getExclusiveOwnerThread()); + } + return new Blocker(blocker, null); + } + + private static Object[] getLockedSynchronizers(Thread thread) { + return JMXMonitoring.getThreadLocks(thread); + } + + private record LockedMonitors(Object[] monitorObjects, int[] monitorDepths) { + } + + private static LockedMonitors getLockedMonitors(Thread thread, int stacktraceLength) { + List monitors = JMXMonitoring.getThreadMonitors(thread); + Object[] monitorObjects = monitors.stream().map(JMXMonitoring.MonitorInfo::originalObject).toArray(); + int[] monitorDepths = monitors.stream().mapToInt(monitorInfo -> stacktraceLength < 0 ? -1 : stacktraceLength - monitorInfo.stacksize()).toArray(); + return new LockedMonitors(monitorObjects, monitorDepths); + } + + private static int getThreadState(Thread thread, boolean inNative) { + int state = PlatformThreads.getThreadStatus(thread); + if (inNative) { + // set the JMM thread state native flag to true: + state |= 0x00400000; + } + return state; + } + + public static ThreadInfo getThreadInfo(Thread thread, int maxDepth, + boolean withLockedMonitors, boolean withLockedSynchronizers) { + Blocker blocker = getBlockerInfo(thread); + StackTraceElement[] stackTrace = getStackTrace(thread, maxDepth); + LockedMonitors lockedMonitors = getLockedMonitors(thread, stackTrace.length); + boolean inNative = stackTrace.length > 0 && stackTrace[0].isNativeMethod(); + Target_java_lang_management_ThreadInfo targetThreadInfo = new Target_java_lang_management_ThreadInfo( + thread, + getThreadState(thread, inNative), + blocker.blockObject, + blocker.ownerThread, + JMXMonitoring.getThreadTotalBlockedCount(thread), + JMXMonitoring.getThreadTotalBlockedTime(thread), + JMXMonitoring.getThreadTotalWaitedCount(thread), + JMXMonitoring.getThreadTotalWaitedTime(thread), + stackTrace, + withLockedMonitors ? lockedMonitors.monitorObjects : new Object[0], + withLockedMonitors ? lockedMonitors.monitorDepths : new int[0], + withLockedSynchronizers ? getLockedSynchronizers(thread) : new Object[0]); + return SubstrateUtil.cast(targetThreadInfo, ThreadInfo.class); + } + } + + public static class DeadlockDetectionUtils { + /** + * Returns an array of thread ids of blocked threads within some given array of ThreadInfo. + * + * @param threadInfos array of ThreadInfo for the threads among which the deadlocks should + * be detected + * @param byMonitorOnly true if we are interested only in the deadlocks blocked exclusively + * on monitors + * @return array containing thread ids of deadlocked threads + */ + public static long[] findDeadlockedThreads(ThreadInfo[] threadInfos, boolean byMonitorOnly) { + HashSet deadlocked = new HashSet<>(); + for (ThreadInfo threadInfo : threadInfos) { + HashSet chain = new HashSet<>(); + ThreadInfo current = threadInfo; + while (current != null && current.getLockInfo() != null && !deadlocked.contains(current.getThreadId())) { + if (!chain.add(current.getThreadId())) { + if (!byMonitorOnly || chain.stream().allMatch(DeadlockDetectionUtils::isBlockedByMonitor)) { + deadlocked.addAll(chain); + } + chain.clear(); + break; + } + long currentLockOwnerId = current.getLockOwnerId(); + current = Arrays.stream(threadInfos).filter(ti -> ti.getThreadId() == currentLockOwnerId).findAny().orElse(null); + } + } + return deadlocked.stream().mapToLong(threadId -> threadId).toArray(); + } + + /** + * Anything that is deadlocked can be blocked either by monitor (the object related to + * JavaMonitor), or a lock (anything under AbstractOwnableSynchronizer). + * + * @return true if provided thread is blocked by a monitor + */ + private static boolean isBlockedByMonitor(long threadId) { + return LockSupport.getBlocker(SubstrateThreadMXBean.getThreadById(threadId)) instanceof JavaMonitor; + } + } +} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/management/SubstrateThreadMXBean.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/management/SubstrateThreadMXBean.java index ca6a29eafd4f..c353b0a33a95 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/management/SubstrateThreadMXBean.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/management/SubstrateThreadMXBean.java @@ -24,6 +24,8 @@ */ package com.oracle.svm.core.jdk.management; +import static com.oracle.svm.core.Uninterruptible.CALLED_FROM_UNINTERRUPTIBLE_CODE; + import java.lang.management.ManagementFactory; import java.lang.management.ThreadInfo; import java.util.Arrays; @@ -36,6 +38,8 @@ import org.graalvm.nativeimage.Platforms; import com.oracle.svm.core.Uninterruptible; +import com.oracle.svm.core.jdk.ThreadMXUtils; +import com.oracle.svm.core.jdk.ThreadMXUtils.ThreadInfoConstructionUtils; import com.oracle.svm.core.jdk.UninterruptibleUtils.AtomicInteger; import com.oracle.svm.core.jdk.UninterruptibleUtils.AtomicLong; import com.oracle.svm.core.thread.PlatformThreads; @@ -55,9 +59,9 @@ public final class SubstrateThreadMXBean implements com.sun.management.ThreadMXB private final AtomicInteger peakThreadCount = new AtomicInteger(0); private final AtomicInteger threadCount = new AtomicInteger(0); private final AtomicInteger daemonThreadCount = new AtomicInteger(0); - private boolean allocatedMemoryEnabled; private boolean cpuTimeEnabled; + private boolean contentionMonitoringEnabled; @Platforms(Platform.HOSTED_ONLY.class) SubstrateThreadMXBean() { @@ -150,45 +154,62 @@ public int getDaemonThreadCount() { return daemonThreadCount.get(); } - /* All remaining methods are unsupported on Substrate VM. */ - @Override public long[] getAllThreadIds() { - return new long[0]; + return Arrays.stream(PlatformThreads.getAllThreads()) + .mapToLong(Thread::threadId) + .toArray(); + } + + public static Thread getThreadById(long id) { + return Arrays.stream(PlatformThreads.getAllThreads()) + .filter(thread -> thread.threadId() == id) + .findAny().orElse(null); } @Override public ThreadInfo getThreadInfo(long id) { - return null; + return getThreadInfo(id, -1); } @Override public ThreadInfo[] getThreadInfo(long[] ids) { - return new ThreadInfo[0]; + return getThreadInfo(ids, -1); } @Override public ThreadInfo getThreadInfo(long id, int maxDepth) { - return null; + return getThreadInfo(id, maxDepth, false, false); + } + + private static ThreadInfo getThreadInfo(long id, int maxDepth, boolean lockedMonitors, boolean lockedSynchronizers) { + Thread thread = getThreadById(id); + return getThreadInfo(thread, maxDepth, lockedMonitors, lockedSynchronizers); + } + + private static ThreadInfo getThreadInfo(Thread thread, int maxDepth, boolean lockedMonitors, boolean lockedSynchronizers) { + return thread == null ? null : ThreadInfoConstructionUtils.getThreadInfo(thread, maxDepth, lockedMonitors, lockedSynchronizers); } @Override public ThreadInfo[] getThreadInfo(long[] ids, int maxDepth) { - return new ThreadInfo[0]; + return (ThreadInfo[]) Arrays.stream(ids).mapToObj(id -> getThreadInfo(id, maxDepth)).toArray(); } @Override public boolean isThreadContentionMonitoringSupported() { - return false; + return true; } @Override + @Uninterruptible(reason = CALLED_FROM_UNINTERRUPTIBLE_CODE, mayBeInlined = true) public boolean isThreadContentionMonitoringEnabled() { - return false; + return contentionMonitoringEnabled; } @Override - public void setThreadContentionMonitoringEnabled(boolean enable) { + public void setThreadContentionMonitoringEnabled(boolean value) { + contentionMonitoringEnabled = value; } @Override @@ -256,32 +277,36 @@ public void setThreadCpuTimeEnabled(boolean enable) { @Override public long[] findMonitorDeadlockedThreads() { - return new long[0]; + ThreadInfo[] threadInfos = dumpAllThreads(true, false); + return ThreadMXUtils.DeadlockDetectionUtils.findDeadlockedThreads(threadInfos, true); } @Override public long[] findDeadlockedThreads() { - return new long[0]; + ThreadInfo[] threadInfos = dumpAllThreads(true, true); + return ThreadMXUtils.DeadlockDetectionUtils.findDeadlockedThreads(threadInfos, false); } @Override public boolean isObjectMonitorUsageSupported() { - return false; + return true; } @Override public boolean isSynchronizerUsageSupported() { - return false; + return true; } @Override public ThreadInfo[] getThreadInfo(long[] ids, boolean lockedMonitors, boolean lockedSynchronizers) { - return new ThreadInfo[0]; + return Arrays.stream(ids).mapToObj(id -> getThreadInfo(id, -1, lockedMonitors, lockedSynchronizers)) + .toArray(ThreadInfo[]::new); } @Override public ThreadInfo[] dumpAllThreads(boolean lockedMonitors, boolean lockedSynchronizers) { - return new ThreadInfo[0]; + return Arrays.stream(PlatformThreads.getAllThreads()).map(thread -> getThreadInfo(thread, -1, lockedMonitors, lockedSynchronizers)) + .toArray(ThreadInfo[]::new); } @Override @@ -290,7 +315,6 @@ public long getThreadAllocatedBytes(long id) { if (!valid) { return -1; } - return PlatformThreads.getThreadAllocatedBytes(id); } @@ -324,8 +348,8 @@ private static void verifyThreadId(long id) { } private static void verifyThreadIds(long[] ids) { - for (int i = 0; i < ids.length; i++) { - verifyThreadId(ids[i]); + for (long id : ids) { + verifyThreadId(id); } } diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/locks/Target_java_util_concurrent_locks_AbstractOwnableSynchronizer.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/locks/Target_java_util_concurrent_locks_AbstractOwnableSynchronizer.java new file mode 100644 index 000000000000..bc0cb6f97805 --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/locks/Target_java_util_concurrent_locks_AbstractOwnableSynchronizer.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2023, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.core.locks; + +import com.oracle.svm.core.SubstrateUtil; +import com.oracle.svm.core.annotate.Alias; +import com.oracle.svm.core.annotate.Substitute; +import com.oracle.svm.core.annotate.TargetClass; +import com.oracle.svm.core.thread.JavaThreads; + +import java.util.concurrent.locks.AbstractOwnableSynchronizer; + +/** + * {@link java.util.concurrent.locks.AbstractOwnableSynchronizer} is substituted in order to track + * the locks per thread when JMX support is enabled. + */ +@SuppressWarnings({"unused"}) +@TargetClass(value = java.util.concurrent.locks.AbstractOwnableSynchronizer.class) +public final class Target_java_util_concurrent_locks_AbstractOwnableSynchronizer { + + /** + * The current owner of exclusive mode synchronization. + */ + @Alias // + private transient Thread exclusiveOwnerThread; + + /** + * Sets the thread that currently owns exclusive access. A {@code null} argument indicates that + * no thread owns access. This method does not otherwise impose any synchronization or + * {@code volatile} field accesses. + * + * @param thread the owner thread + */ + @Substitute() + protected void setExclusiveOwnerThread(Thread thread) { + if (thread == null && exclusiveOwnerThread != null) { + JavaThreads.JMXMonitoring.removeThreadLock(exclusiveOwnerThread, + SubstrateUtil.cast(this, AbstractOwnableSynchronizer.class)); + } else { + JavaThreads.JMXMonitoring.addThreadLock(thread, + SubstrateUtil.cast(this, AbstractOwnableSynchronizer.class)); + } + exclusiveOwnerThread = thread; + } + + /** + * Returns the thread last set by {@code setExclusiveOwnerThread}, or {@code null} if never set. + * This method does not otherwise impose any synchronization or {@code volatile} field accesses. + * + * @return the owner thread + */ + @Alias + public Thread getExclusiveOwnerThread() { + return exclusiveOwnerThread; + } +} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/monitor/JavaMonitor.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/monitor/JavaMonitor.java index f0d580123079..e59443cd7694 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/monitor/JavaMonitor.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/monitor/JavaMonitor.java @@ -73,10 +73,13 @@ public void monitorEnter(Object obj) { acquire(1); JavaMonitorEnterEvent.emit(obj, latestJfrTid, startTicks); } - latestJfrTid = SubstrateJVM.getCurrentThreadId(); } + public Object getBlockedObject() { + return blockedObject; + } + public void monitorExit() { release(1); } @@ -218,6 +221,10 @@ boolean isLocked() { return getState() != 0; } + public long getOwnerThreadId() { + return getState(); + } + /** * Storing and comparing a {@link Thread} object to determine lock ownership needs heap address * computations and GC barriers, while we don't actually need to access the object, so we use a diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/monitor/JavaMonitorQueuedSynchronizer.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/monitor/JavaMonitorQueuedSynchronizer.java index 561642445c48..391b4d02cce5 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/monitor/JavaMonitorQueuedSynchronizer.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/monitor/JavaMonitorQueuedSynchronizer.java @@ -145,6 +145,7 @@ public boolean block() { private transient volatile Node head; private transient volatile Node tail; private volatile long state; + protected Object blockedObject; // see AbstractQueuedLongSynchronizer.getState() @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/monitor/MultiThreadedMonitorSupport.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/monitor/MultiThreadedMonitorSupport.java index bd477fb4ae6d..50f879f16054 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/monitor/MultiThreadedMonitorSupport.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/monitor/MultiThreadedMonitorSupport.java @@ -256,6 +256,8 @@ public void monitorEnter(Object obj, MonitorInflationCause cause) { if (monitor == null) { // successful JavaMonitorInflateEvent.emit(obj, startTicks, MonitorInflationCause.MONITOR_ENTER); newMonitor.latestJfrTid = current; + newMonitor.blockedObject = obj; + JavaThreads.JMXMonitoring.addThreadMonitor(obj); return; } } @@ -263,6 +265,7 @@ public void monitorEnter(Object obj, MonitorInflationCause cause) { monitor = getOrCreateMonitor(obj, cause); } monitor.monitorEnter(obj); + JavaThreads.JMXMonitoring.addThreadMonitor(obj); } @SubstrateForeignCallTarget(stubCallingConvention = false) @@ -314,6 +317,7 @@ public void monitorExit(Object obj, MonitorInflationCause cause) { monitor = getOrCreateMonitor(obj, cause); } monitor.monitorExit(); + JavaThreads.JMXMonitoring.removeThreadMonitor(); } @Override @@ -495,6 +499,7 @@ protected JavaMonitor getOrCreateMonitorFromMap(Object obj, boolean createIfNotE JavaMonitor previousEntry = additionalMonitors.put(obj, newMonitor); VMError.guarantee(previousEntry == null, "Replaced monitor in secondary storage map"); JavaMonitorInflateEvent.emit(obj, startTicks, cause); + newMonitor.blockedObject = obj; return newMonitor; } finally { additionalMonitorsLock.unlock(); diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/thread/JavaThreads.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/thread/JavaThreads.java index a3a5158ce61e..adb06d3877fe 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/thread/JavaThreads.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/thread/JavaThreads.java @@ -25,11 +25,19 @@ package com.oracle.svm.core.thread; import java.lang.Thread.UncaughtExceptionHandler; +import java.lang.management.ManagementFactory; +import java.lang.management.ThreadMXBean; import java.security.AccessControlContext; import java.security.AccessController; +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.ConcurrentLinkedDeque; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.locks.AbstractOwnableSynchronizer; +import org.graalvm.collections.EconomicSet; +import org.graalvm.collections.Equivalence; import org.graalvm.nativeimage.CurrentIsolate; import org.graalvm.nativeimage.IsolateThread; import org.graalvm.nativeimage.Platforms; @@ -54,6 +62,8 @@ import jdk.graal.compiler.core.common.SuppressFBWarnings; import jdk.graal.compiler.replacements.ReplacementsUtil; +import static com.oracle.svm.core.Uninterruptible.CALLED_FROM_UNINTERRUPTIBLE_CODE; + /** * Implements operations on {@linkplain Target_java_lang_Thread Java threads}, which are on a higher * abstraction level than {@link IsolateThread}s. This class distinguishes these types of threads: @@ -430,6 +440,182 @@ static void setCurrentThread(Thread carrier, Thread thread) { currentVThreadId.set(getThreadId(thread)); } + public static class JMXMonitoring { + private static final ThreadMXBean THREAD_MX_BEAN = ManagementFactory.getThreadMXBean(); + private static final int NO_VALUE = -1; + + @Uninterruptible(reason = CALLED_FROM_UNINTERRUPTIBLE_CODE, mayBeInlined = true) + public static void handleContentionMonitoring(Target_java_lang_Thread targetThread, int threadStatus) { + boolean contentionEnabled = THREAD_MX_BEAN.isThreadContentionMonitoringEnabled(); + if (contentionEnabled) { + handleContentionWhenEnabled(targetThread, threadStatus); + } else { + resetContentionIfStarted(targetThread); + } + } + + @Uninterruptible(reason = CALLED_FROM_UNINTERRUPTIBLE_CODE, mayBeInlined = true) + private static void resetContentionIfStarted(Target_java_lang_Thread targetThread) { + if (targetThread.lastStartedBlocked != NO_VALUE || targetThread.lastStartedWaiting != NO_VALUE) { + targetThread.lastStartedBlocked = targetThread.lastStartedWaiting = NO_VALUE; + } + } + + @Uninterruptible(reason = CALLED_FROM_UNINTERRUPTIBLE_CODE, mayBeInlined = true) + private static void handleContentionWhenEnabled(Target_java_lang_Thread targetThread, int threadStatus) { + switch (threadStatus) { + case ThreadStatus.BLOCKED_ON_MONITOR_ENTER: + setThreadBlockedToEnterLock(targetThread); + break; + case ThreadStatus.IN_OBJECT_WAIT: + case ThreadStatus.IN_OBJECT_WAIT_TIMED: + case ThreadStatus.SLEEPING: + case ThreadStatus.PARKED: + case ThreadStatus.PARKED_TIMED: + setThreadStartsWaiting(targetThread); + break; + case ThreadStatus.RUNNABLE: + calculateContentionOnThreadRunning(targetThread); + break; + } + } + + @Uninterruptible(reason = CALLED_FROM_UNINTERRUPTIBLE_CODE, mayBeInlined = true) + private static void setThreadBlockedToEnterLock(Target_java_lang_Thread thread) { + if (thread.lastStartedBlocked == NO_VALUE) { + thread.lastStartedBlocked = System.currentTimeMillis(); + thread.blockedCount++; + } + } + + @Uninterruptible(reason = CALLED_FROM_UNINTERRUPTIBLE_CODE, mayBeInlined = true) + private static void setThreadStartsWaiting(Target_java_lang_Thread thread) { + if (thread.lastStartedWaiting == NO_VALUE) { + thread.lastStartedWaiting = System.currentTimeMillis(); + thread.waitedCount++; + } + } + + @Uninterruptible(reason = CALLED_FROM_UNINTERRUPTIBLE_CODE, mayBeInlined = true) + private static void calculateContentionOnThreadRunning(Target_java_lang_Thread thread) { + long currentTimeMillis = System.currentTimeMillis(); + thread.timeWaited += thread.lastStartedWaiting == NO_VALUE ? 0 : (currentTimeMillis - thread.lastStartedWaiting); + thread.timeBlocked += thread.lastStartedBlocked == NO_VALUE ? 0 : (currentTimeMillis - thread.lastStartedBlocked); + thread.lastStartedBlocked = thread.lastStartedWaiting = NO_VALUE; + } + + public static long getThreadTotalWaitedTime(Thread thread) { + if (!THREAD_MX_BEAN.isThreadContentionMonitoringEnabled()) { + return 0; + } + Target_java_lang_Thread targetThread = toTarget(thread); + return targetThread.timeWaited + (targetThread.lastStartedWaiting == NO_VALUE ? 0 : (System.currentTimeMillis() - targetThread.lastStartedWaiting)); + } + + public static long getThreadTotalWaitedCount(Thread thread) { + return toTarget(thread).waitedCount; + } + + public static long getThreadTotalBlockedTime(Thread thread) { + if (!THREAD_MX_BEAN.isThreadContentionMonitoringEnabled()) { + return 0; + } + Target_java_lang_Thread targetThread = toTarget(thread); + return targetThread.timeBlocked + (targetThread.lastStartedBlocked == NO_VALUE ? 0 : (System.currentTimeMillis() - targetThread.lastStartedBlocked)); + } + + public static long getThreadTotalBlockedCount(Thread thread) { + return toTarget(thread).blockedCount; + } + + public static void addThreadLock(Thread thread, AbstractOwnableSynchronizer synchronizer) { + assert !isVirtual(thread); + Target_java_lang_Thread targetThread = toTarget(thread); + if (targetThread != null) { + if (targetThread.locks == null) { + targetThread.locks = EconomicSet.create(Equivalence.IDENTITY_WITH_SYSTEM_HASHCODE); + } + synchronized (targetThread.locks) { + targetThread.locks.add(synchronizer); + } + } + } + + public static void removeThreadLock(Thread thread, AbstractOwnableSynchronizer synchronizer) { + assert !isVirtual(thread); + Target_java_lang_Thread targetThread = toTarget(thread); + if (targetThread != null && targetThread.locks != null) { + synchronized (targetThread.locks) { + targetThread.locks.remove(synchronizer); + } + } + } + + public static Object[] getThreadLocks(Thread thread) { + assert !isVirtual(thread); + Target_java_lang_Thread targetThread = toTarget(thread); + if (targetThread != null && targetThread.locks != null) { + synchronized (targetThread.locks) { + return targetThread.locks.toArray(new AbstractOwnableSynchronizer[targetThread.locks.size()]); + } + } + return new Object[0]; + } + + public record MonitorInfo(Object originalObject, int stacksize) { + } + + public static void addThreadMonitor(Object originalObject) { + Thread currentThread = Thread.currentThread(); + Target_java_lang_Thread targetThread = toTarget(currentThread); + if (targetThread != null) { + if (targetThread.monitors == null) { + targetThread.monitors = new ConcurrentLinkedDeque<>(); + } + StackTraceElement[] stacktrace = currentThread.getStackTrace(); + + /* + * During monitor creation we add several extra frames on top of the stack. At the + * moment when monitor reporting via JMX is requested the stack doesn't contain them + * anymore. The offset value below helps align with the real location where + * synchronized block occurred. + */ + int internalCodeOffset = 0; + for (StackTraceElement element : stacktrace) { + String moduleName = element.getModuleName(); + if (moduleName != null && (moduleName.equals("java.base") || moduleName.startsWith("org.graalvm"))) { + internalCodeOffset++; + } else { + break; + } + } + /* + * If all frames belong to internal modules, they are not subtracted from remembered + * stacktrace size, and we assume that the monitor happened at the top frame: + */ + int rememberedStacksize = stacktrace.length - (internalCodeOffset < stacktrace.length ? internalCodeOffset : 0); + targetThread.monitors.push(new MonitorInfo(originalObject, rememberedStacksize)); + } + } + + public static void removeThreadMonitor() { + Target_java_lang_Thread targetThread = toTarget(Thread.currentThread()); + if (targetThread != null && targetThread.monitors != null) { + targetThread.monitors.pop(); + } + } + + public static List getThreadMonitors(Thread thread) { + assert !isVirtual(thread); + Target_java_lang_Thread targetThread = toTarget(thread); + List listOfMonitors = new LinkedList<>(); + if (targetThread != null && targetThread.monitors != null) { + targetThread.monitors.iterator().forEachRemaining(listOfMonitors::add); + } + return listOfMonitors; + } + } + @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) public static Thread getCurrentThreadOrNull() { Thread thread = PlatformThreads.currentThread.get(); diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/thread/PlatformThreads.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/thread/PlatformThreads.java index 344c6895b28a..c2bd82e1e38a 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/thread/PlatformThreads.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/thread/PlatformThreads.java @@ -907,7 +907,7 @@ static Map getAllStackTraces() { return vmOp.result; } - static Thread[] getAllThreads() { + public static Thread[] getAllThreads() { GetAllThreadsOperation vmOp = new GetAllThreadsOperation(); vmOp.enqueue(); return vmOp.result.toArray(new Thread[0]); @@ -1075,8 +1075,10 @@ public static int getThreadStatus(Thread thread) { @Uninterruptible(reason = CALLED_FROM_UNINTERRUPTIBLE_CODE, mayBeInlined = true) public static void setThreadStatus(Thread thread, int threadStatus) { assert !isVirtual(thread); - assert toTarget(thread).holder.threadStatus != ThreadStatus.TERMINATED : "once a thread is marked as terminated, its status must not change"; - toTarget(thread).holder.threadStatus = threadStatus; + Target_java_lang_Thread targetThread = toTarget(thread); + assert targetThread.holder.threadStatus != ThreadStatus.TERMINATED : "once a thread is marked as terminated, its status must not change"; + JavaThreads.JMXMonitoring.handleContentionMonitoring(targetThread, threadStatus); + targetThread.holder.threadStatus = threadStatus; } static boolean compareAndSetThreadStatus(Thread thread, int expectedStatus, int newStatus) { diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/thread/Target_java_lang_Thread.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/thread/Target_java_lang_Thread.java index 0048deef2abe..819f8b3b14ac 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/thread/Target_java_lang_Thread.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/thread/Target_java_lang_Thread.java @@ -31,6 +31,9 @@ import java.util.Map; import java.util.Objects; +import java.util.concurrent.ConcurrentLinkedDeque; +import java.util.concurrent.locks.AbstractOwnableSynchronizer; +import org.graalvm.collections.EconomicSet; import org.graalvm.nativeimage.IsolateThread; import org.graalvm.nativeimage.Platforms; import org.graalvm.nativeimage.impl.InternalPlatform; @@ -91,6 +94,42 @@ public final class Target_java_lang_Thread { @RecomputeFieldValue(kind = RecomputeFieldValue.Kind.NewInstance, declClass = ThreadData.class)// UnacquiredThreadData threadData; + @Inject // + @RecomputeFieldValue(kind = RecomputeFieldValue.Kind.Reset) // + long lastStartedWaiting = -1; + + @Inject // + @RecomputeFieldValue(kind = RecomputeFieldValue.Kind.Reset) // + long lastStartedBlocked = -1; + + @Inject // + @RecomputeFieldValue(kind = RecomputeFieldValue.Kind.Reset) // + long timeWaited = 0; + + @Inject // + @RecomputeFieldValue(kind = RecomputeFieldValue.Kind.Reset) // + long timeBlocked = 0; + + @Inject // + @RecomputeFieldValue(kind = RecomputeFieldValue.Kind.Reset) // + long blockedCount = 0; + + @Inject // + @RecomputeFieldValue(kind = RecomputeFieldValue.Kind.Reset) // + long waitedCount = 0; + + /** + * If the thread owns a lock or a monitor, it can't be updated by any other thread before the + * owner of that lock resets itself. + */ + @Inject // + @RecomputeFieldValue(kind = RecomputeFieldValue.Kind.Reset) // + EconomicSet locks; + + @Inject // + @RecomputeFieldValue(kind = RecomputeFieldValue.Kind.Reset) // + ConcurrentLinkedDeque monitors; + @Alias// ClassLoader contextClassLoader;