Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
f614d7a
feat(thread-dump): port PR #231 enhancements to Vue 3 / Spring Boot 3
zachelnet Jun 18, 2026
dff135f
feat(auth): Keycloak OIDC authentication via Spring OAuth2 client
zachelnet Jun 18, 2026
8a53288
Bump Spring Boot to 4.1 (#390)
D-D-H Jun 25, 2026
0171dff
fix(i18n): localize required-field validation message
zachelnet Jun 25, 2026
65f90a8
fix(api): handle invalid regex patterns gracefully
zachelnet Jun 25, 2026
daf23e7
fix: prevent XSS by escaping HTML entities in thread dump lines
zachelnet Jun 25, 2026
6a7d57c
fix: correct threshold logic for HIGH_BLOCKED_THREAD_COUNT
zachelnet Jun 25, 2026
165822e
fix: use ParsePosition to ensure complete string consumption in parse…
zachelnet Jun 25, 2026
53ddd02
fix(Diagnose): use Element Plus icons and add .finally() to loadData
zachelnet Jun 26, 2026
de14c84
fix(BlockedThreads): reset svgRefs before each update
zachelnet Jun 26, 2026
28d61d9
refactor(Converter): import ParsePosition instead of using fully qual…
zachelnet Jun 26, 2026
13c4804
fix(Converter): remove Locale.US NumberFormat step that mis-parses Ge…
zachelnet Jun 26, 2026
9826459
fix(BlockedThreads): pass named values correctly to vue-i18n t()
zachelnet Jun 26, 2026
7265b95
fix(BlockedThreads): remove duplicate root node label from SVG tree
zachelnet Jun 26, 2026
db4ec36
fix(BlockedThreads): adapt SVG tree colors for dark mode
zachelnet Jun 26, 2026
6342b05
refactor(CpuConsumingThreads): merge Java/NonJava into single collaps…
zachelnet Jul 13, 2026
da4e2b7
refactor(ThreadDumpOverview): move state distribution chart next to d…
zachelnet Jul 13, 2026
dfad03d
style: replace hardcoded colors and font sizes with Element Plus CSS …
zachelnet Jul 13, 2026
fc5bdc5
fix(ts): install @types/d3 and replace replaceAll with ES2020-compati…
zachelnet Jul 13, 2026
2cedd1a
fix(BlockedThreads): add label to root (blocking) node in SVG tree fo…
zachelnet Jul 13, 2026
45c7014
feat(ThreadDump): move state distribution to basic info section
zachelnet Jul 13, 2026
3ff4002
refactor(threaddump): fix resize listener leak and extract STATE_COLO…
zachelnet Jul 13, 2026
f25f25b
refactor(threaddump): code review fixes and UI polish
D-D-H Jul 15, 2026
dc66b2c
Merge branch 'eclipse-jifa:main' into feature/thread-dump-backport
zachelnet Jul 23, 2026
13f259c
update
D-D-H Jul 28, 2026
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
1 change: 1 addition & 0 deletions NOTICE
Original file line number Diff line number Diff line change
Expand Up @@ -175,3 +175,4 @@ permitted.

Alibaba - Initial implementation
Netflix - Basic customization hooks
SEEBURGER - ThreadDump enhancements

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/********************************************************************************
* Copyright (c) 2026 Contributors to the Eclipse Foundation
*
* See the NOTICE file(s) distributed with this work for additional
* information regarding copyright ownership.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0 and CC0-1.0
*
* AI Disclosure: This file was largely AI-generated with GitHub Copilot.
* The AI-generated portions are made available under CC0-1.0. The human
* contributor has reviewed and verified the code.
* Assisted-by: GitHub Copilot (Claude Sonnet 4.5)
********************************************************************************/

package org.eclipse.jifa.tda.diagnoser;

import java.util.List;
import java.util.Map;

import org.eclipse.jifa.tda.vo.VThread;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;

/**
* Represents a single diagnostic finding produced by {@link ThreadDumpDiagnoser}.
* <p>
* Messages and suggestions are intentionally kept out of this class – they are
* resolved on the frontend via the {@link Type} key so that they can be
* localised without re-deploying the backend.
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class Diagnostic {

private Severity severity;

/** Identifies the kind of issue; used as i18n key on the frontend. */
private Type type;

/**
* Named parameters that are interpolated into the i18n message on the
* frontend (e.g. {@code count}, {@code name}, {@code threshold}).
*/
private Map<String, Object> params;

/** Affected threads, may be {@code null} when not applicable. */
private List<VThread> threads;

public enum Severity {
OK, INFO, WARNING, ERROR
}

public enum Type {
HIGH_THREAD_COUNT,
DEADLOCK,
HIGH_BLOCKED_THREAD_COUNT,
HIGH_STACK_SIZE,
HIGH_CPU_RATIO,
THREAD_THROWING_EXCEPTION
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/********************************************************************************
* Copyright (c) 2026 Contributors to the Eclipse Foundation
*
* See the NOTICE file(s) distributed with this work for additional
* information regarding copyright ownership.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0 and CC0-1.0
*
* AI Disclosure: This file was largely AI-generated with GitHub Copilot.
* The AI-generated portions are made available under CC0-1.0. The human
* contributor has reviewed and verified the code.
* Assisted-by: GitHub Copilot (Claude Sonnet 4.5)
********************************************************************************/

package org.eclipse.jifa.tda.diagnoser;

import lombok.Data;

/**
* Configuration controlling which heuristics the {@link ThreadDumpDiagnoser}
* applies and at what thresholds warnings are emitted.
* <p>
* A threshold value of {@code 0} or less disables the corresponding check.
*/
@Data
public class ThreadDumpAnalysisConfig {

/** Issue a warning if the total thread count reaches at least this value. */
private int highThreadsThreshold = 500;

/** Issue a warning if at least that many threads are blocked. */
private int highBlockedThreadsThreshold = 3;

/** Issue a warning if a thread's stack depth reaches at least this value. */
private int highStackSizeThreshold = 200;

/**
* Issue a warning if the ratio between a thread's cpu time and its elapsed
* time is at least this value (range 0.0 – 1.0).
*/
private double highCpuConsumedRatio = 0.5;

/** Issue a warning if a thread is currently throwing an exception. */
private boolean reportThrowingException = true;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
/********************************************************************************
* Copyright (c) 2026 Contributors to the Eclipse Foundation
*
* See the NOTICE file(s) distributed with this work for additional
* information regarding copyright ownership.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0 and CC0-1.0
*
* AI Disclosure: This file was largely AI-generated with GitHub Copilot.
* The AI-generated portions are made available under CC0-1.0. The human
* contributor has reviewed and verified the code.
* Assisted-by: GitHub Copilot (Claude Sonnet 4.5)
********************************************************************************/

package org.eclipse.jifa.tda.diagnoser;

import org.eclipse.jifa.tda.diagnoser.Diagnostic.Severity;
import org.eclipse.jifa.tda.enums.JavaThreadState;
import org.eclipse.jifa.tda.model.Frame;
import org.eclipse.jifa.tda.model.JavaThread;
import org.eclipse.jifa.tda.model.Snapshot;
import org.eclipse.jifa.tda.model.Thread;
import org.eclipse.jifa.tda.vo.VThread;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;

/**
* Analyses a parsed thread dump {@link Snapshot} against a
* {@link ThreadDumpAnalysisConfig} and returns a list of {@link Diagnostic}
* findings.
* <p>
* All human-readable messages and suggestions are intentionally absent from
* this class; they are resolved on the frontend via the {@link Diagnostic.Type}
* key so that they can be localised without redeploying the backend.
*/
public class ThreadDumpDiagnoser {

/** Parameter key: number of affected threads. */
private static final String KEY_COUNT = "count";
/** Parameter key: name of the single affected thread (only set when count == 1). */
private static final String KEY_NAME = "name";
/** Parameter key: a numeric threshold used in the diagnostic message. */
private static final String KEY_THRESHOLD = "threshold";

/**
* Runs all configured heuristics against the snapshot and returns a
* (potentially empty) list of findings.
*
* @param snapshot the parsed thread dump
* @param config the analysis configuration
* @return list of diagnostic findings, never {@code null}
*/
public List<Diagnostic> analyze(Snapshot snapshot, ThreadDumpAnalysisConfig config) {
List<Diagnostic> results = new ArrayList<>();
analyzeDeadlock(snapshot, config, results);
analyzeBlockedThreads(snapshot, config, results);
analyzeThreadCount(snapshot, config, results);
analyzeLargeStackSize(snapshot, config, results);
analyzeCpuRatio(snapshot, config, results);
analyzeExceptionThread(snapshot, config, results);
return results;
}

// ------------------------------------------------------------------
// individual checks
// ------------------------------------------------------------------

private void analyzeDeadlock(Snapshot snapshot, ThreadDumpAnalysisConfig config,
List<Diagnostic> results) {
if (snapshot.getDeadLockThreads() == null) {
return;
}
List<JavaThread> deadlockThreads = new ArrayList<>();
snapshot.getDeadLockThreads().forEach(deadlockThreads::addAll);
if (!deadlockThreads.isEmpty()) {
results.add(new Diagnostic(Severity.ERROR, Diagnostic.Type.DEADLOCK,
createParams(deadlockThreads), toVThread(deadlockThreads)));
}
}

private void analyzeBlockedThreads(Snapshot snapshot, ThreadDumpAnalysisConfig config,
List<Diagnostic> results) {
if (config.getHighBlockedThreadsThreshold() <= 0) {
return;
}
List<JavaThread> blockedThreads = snapshot.getJavaThreads().stream()
.filter(t -> t.getJavaThreadState() == JavaThreadState.BLOCKED_ON_MONITOR_ENTER)
.collect(Collectors.toList());
if (blockedThreads.size() >= config.getHighBlockedThreadsThreshold()) {
results.add(new Diagnostic(Severity.ERROR, Diagnostic.Type.HIGH_BLOCKED_THREAD_COUNT,
createParams(blockedThreads), toVThread(blockedThreads)));
}
}

private void analyzeThreadCount(Snapshot snapshot, ThreadDumpAnalysisConfig config,
List<Diagnostic> results) {
if (config.getHighThreadsThreshold() <= 0) {
return;
}
Collection<Thread> allThreads = snapshot.getThreadMap().values();
if (allThreads.size() >= config.getHighThreadsThreshold()) {
results.add(new Diagnostic(Severity.WARNING, Diagnostic.Type.HIGH_THREAD_COUNT,
createParams(allThreads), null));
}
}

private void analyzeLargeStackSize(Snapshot snapshot, ThreadDumpAnalysisConfig config,
List<Diagnostic> results) {
if (config.getHighStackSizeThreshold() <= 0) {
return;
}
List<JavaThread> threads = snapshot.getJavaThreads().stream()
.filter(t -> Optional.ofNullable(t.getTrace())
.map(trace -> trace.getFrames())
.map(frames -> frames.length)
.orElse(0) > config.getHighStackSizeThreshold())
.collect(Collectors.toList());
if (!threads.isEmpty()) {
Map<String, Object> params = createParams(threads);
params.put(KEY_THRESHOLD, config.getHighStackSizeThreshold());
results.add(new Diagnostic(Severity.WARNING, Diagnostic.Type.HIGH_STACK_SIZE,
params, toVThread(threads)));
}
}

private void analyzeCpuRatio(Snapshot snapshot, ThreadDumpAnalysisConfig config,
List<Diagnostic> results) {
if (config.getHighCpuConsumedRatio() <= 0) {
return;
}
List<JavaThread> threads = snapshot.getJavaThreads().stream()
.filter(t -> t.getCpu() > 0 && t.getElapsed() > 0)
.filter(t -> (t.getCpu() / t.getElapsed()) >= config.getHighCpuConsumedRatio())
.collect(Collectors.toList());
if (!threads.isEmpty()) {
results.add(new Diagnostic(Severity.WARNING, Diagnostic.Type.HIGH_CPU_RATIO,
createParams(threads), toVThread(threads)));
}
}

private void analyzeExceptionThread(Snapshot snapshot, ThreadDumpAnalysisConfig config,
List<Diagnostic> results) {
if (!config.isReportThrowingException()) {
return;
}
List<JavaThread> threads = snapshot.getJavaThreads().stream()
.filter(this::isThrowingException)
.collect(Collectors.toList());
if (!threads.isEmpty()) {
results.add(new Diagnostic(Severity.WARNING, Diagnostic.Type.THREAD_THROWING_EXCEPTION,
createParams(threads), toVThread(threads)));
}
}

// ------------------------------------------------------------------
// helpers
// ------------------------------------------------------------------

/**
* Returns {@code true} if the top frames of the thread's stack trace indicate
* that a {@link Throwable} is currently being constructed (i.e. the thread is
* inside {@code fillInStackTrace}). Only the first five frames are inspected
* to avoid false positives from deep exception-handling code.
*/
private boolean isThrowingException(JavaThread thread) {
if (thread.getTrace() == null || thread.getTrace().getFrames() == null) {
return false;
}
Frame[] frames = thread.getTrace().getFrames();
String throwableName = Throwable.class.getName();
for (int i = 0; i < Math.min(frames.length, 5); i++) {
Frame frame = frames[i];
if (throwableName.equals(frame.getClazz())
&& frame.getMethod() != null
&& frame.getMethod().contains("fillInStackTrace")) {
return true;
}
}
return false;
}

private List<VThread> toVThread(Collection<? extends Thread> threads) {
return threads.stream()
.map(t -> new VThread(t.getId(), t.getName(), null, null))
.collect(Collectors.toList());
}

/**
* Builds a parameter map for diagnostic messages. Always sets {@link #KEY_COUNT}.
* When there is exactly one thread, also sets {@link #KEY_NAME} for singular
* message variants.
*/
private Map<String, Object> createParams(Collection<? extends Thread> threads) {
Map<String, Object> params = new HashMap<>();
params.put(KEY_COUNT, threads.size());
if (threads.size() == 1) {
params.put(KEY_NAME, threads.stream().findFirst().get().getName());
}
return params;
}
}
Loading
Loading