Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
import java.lang.management.ThreadInfo;
import java.lang.management.ThreadMXBean;
import java.util.Collections;
import java.util.HashMap;
import java.util.EnumMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;

Expand All @@ -21,6 +22,7 @@ public class ThreadStatesGaugeSet implements MetricSet {

// do not compute stack traces.
private final static int STACK_TRACE_DEPTH = 0;
private final static int[] ZERO_COUNT = new int[1];

Copilot AI Jan 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ZERO_COUNT constant is defined as an int array new int[1] but is used as a shared constant. While this works for the intended purpose (returning 0 for thread states with no threads), using a mutable array as a constant is an anti-pattern. If any code were to modify ZERO_COUNT[0], it would affect all thread states that use it. Consider using a constant int value 0 directly, or if an array is needed for the getOrDefault pattern, create a new array each time or document that the array must never be modified.

Copilot uses AI. Check for mistakes.

private final ThreadMXBean threads;
private final ThreadDeadlockDetector deadlockDetector;
Expand All @@ -46,34 +48,35 @@ public ThreadStatesGaugeSet(ThreadMXBean threads,

@Override
public Map<String, Metric> getMetrics() {
final Map<String, Metric> gauges = new HashMap<>();
final Map<String, Metric> gauges = new LinkedHashMap<>(); // deterministic order

// expensive methods only once, whether or not "cached"
ThreadInfo[] threadInfos = getThreadInfo();
Set<String> deadlockedThreads = deadlockDetector.getDeadlockedThreads();

EnumMap<Thread.State, int[]> byState = new EnumMap<>(Thread.State.class);
for (ThreadInfo threadInfo : threadInfos) {
Thread.State tState;
if (threadInfo != null && (tState = threadInfo.getThreadState()) != null) {
byState.computeIfAbsent(tState, (k) -> new int[1])[0]++;
}
}

for (final Thread.State state : Thread.State.values()) {
gauges.put(name(state.toString().toLowerCase(), "count"),
(Gauge<Object>) () -> getThreadCount(state));
(Gauge<Object>) () -> byState.getOrDefault(state, ZERO_COUNT)[0]);
}

gauges.put("count", (Gauge<Integer>) threads::getThreadCount);
gauges.put("daemon.count", (Gauge<Integer>) threads::getDaemonThreadCount);
gauges.put("peak.count", (Gauge<Integer>) threads::getPeakThreadCount);
gauges.put("total_started.count", (Gauge<Long>) threads::getTotalStartedThreadCount);
gauges.put("deadlock.count", (Gauge<Integer>) () -> deadlockDetector.getDeadlockedThreads().size());
gauges.put("deadlocks", (Gauge<Set<String>>) deadlockDetector::getDeadlockedThreads);
gauges.put("deadlock.count", (Gauge<Integer>) deadlockedThreads::size);
gauges.put("deadlocks", (Gauge<Set<String>>) () -> deadlockedThreads);
Comment on lines +54 to +75

Copilot AI Jan 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The gauge lambdas are capturing stale values instead of computing fresh values on each call to getValue(). The threadInfos and deadlockedThreads are computed once when getMetrics() is called, and the gauge lambdas reference these captured values. This means the gauge values will never update after initial registration.

Gauges should compute fresh values each time getValue() is called, as seen in other MetricSet implementations like MemoryUsageGaugeSet and ClassLoadingGaugeSet. The optimization goal of avoiding redundant calls should be achieved differently - perhaps by caching with a TTL as CachedThreadStatesGaugeSet does, or by calling the expensive methods once per getValue() invocation rather than once per metric.

For example, line 67 creates a gauge that returns byState.getOrDefault(state, ZERO_COUNT)[0], which captures the EnumMap computed at line 57. This value will never change even as threads change state.

Copilot uses AI. Check for mistakes.

return Collections.unmodifiableMap(gauges);
}

private int getThreadCount(Thread.State state) {
final ThreadInfo[] allThreads = getThreadInfo();
int count = 0;
for (ThreadInfo info : allThreads) {
if (info != null && info.getThreadState() == state) {
count++;
}
}
return count;
}

ThreadInfo[] getThreadInfo() {
return threads.getThreadInfo(threads.getAllThreadIds(), STACK_TRACE_DEPTH);
}
Expand Down
Loading