From f614d7a282cde4271a1476045f154cd71c1e34df Mon Sep 17 00:00:00 2001 From: Martin Zach Date: Thu, 18 Jun 2026 17:57:13 +0200 Subject: [PATCH 01/24] feat(thread-dump): port PR #231 enhancements to Vue 3 / Spring Boot 3 Backend (analysis/thread-dump): - New diagnoser: ThreadDumpDiagnoser, Diagnostic, ThreadDumpAnalysisConfig - ThreadDumpAnalyzer: diagnose(), blockingThreads(), cpuConsumingThreads(), cpuConsumingThreadsCompare(), searchThreads(), threads() with state/id filter - Converter: robust locale-aware number parsing - New VOs: SearchHit, VBlockingThread, extended VThread - Tests: TestDiagnoser, TestConverter, TestAnalyzer updated Frontend (Vue 3): - New components: Diagnose, BlockedThreads (D3 tree), CpuConsumingThreads (ECharts bar), ThreadDumpSearch, ThreadDumpSearchForm, ThreadDumpOverview - ThreadDump.vue: thread summary with clickable colored el-tags per state - ThreadDumpOverview.vue: ECharts Doughnut + Bar charts with dark mode, resize handler and onUnmounted cleanup - STATE_COLORS covers all JavaThreadState + OSTreadState enum values - i18n: en.ts + zh.ts for all new keys Assisted-by: GitHub Copilot (Claude Sonnet 4.5) --- NOTICE | 1 + .../eclipse/jifa/tda/ThreadDumpAnalyzer.java | 292 +++++++++++- .../jifa/tda/diagnoser/Diagnostic.java | 70 +++ .../diagnoser/ThreadDumpAnalysisConfig.java | 49 ++ .../tda/diagnoser/ThreadDumpDiagnoser.java | 208 ++++++++ .../org/eclipse/jifa/tda/util/Converter.java | 63 ++- .../org/eclipse/jifa/tda/vo/SearchHit.java | 55 +++ .../eclipse/jifa/tda/vo/VBlockingThread.java | 40 ++ .../java/org/eclipse/jifa/tda/vo/VThread.java | 16 +- .../org/eclipse/jifa/tda/TestAnalyzer.java | 2 +- .../org/eclipse/jifa/tda/TestConverter.java | 118 +++++ .../org/eclipse/jifa/tda/TestDiagnoser.java | 170 +++++++ .../test/resources/jstack_17_with_blocked.log | 146 ++++++ frontend/components.d.ts | 2 + frontend/package-lock.json | 445 +++++++++++++++++- frontend/package.json | 1 + .../components/threaddump/BlockedThreads.vue | 214 +++++++++ .../threaddump/CpuConsumingThreads.vue | 141 ++++++ .../src/components/threaddump/Diagnose.vue | 145 ++++++ frontend/src/components/threaddump/Thread.vue | 11 + .../src/components/threaddump/ThreadDump.vue | 147 ++++-- .../threaddump/ThreadDumpOverview.vue | 302 ++++++++++++ .../threaddump/ThreadDumpSearch.vue | 232 +++++++++ .../threaddump/ThreadDumpSearchForm.vue | 168 +++++++ frontend/src/i18n/threaddump/en.ts | 95 ++++ frontend/src/i18n/threaddump/zh.ts | 86 ++++ 26 files changed, 3174 insertions(+), 45 deletions(-) create mode 100644 analysis/thread-dump/src/main/java/org/eclipse/jifa/tda/diagnoser/Diagnostic.java create mode 100644 analysis/thread-dump/src/main/java/org/eclipse/jifa/tda/diagnoser/ThreadDumpAnalysisConfig.java create mode 100644 analysis/thread-dump/src/main/java/org/eclipse/jifa/tda/diagnoser/ThreadDumpDiagnoser.java create mode 100644 analysis/thread-dump/src/main/java/org/eclipse/jifa/tda/vo/SearchHit.java create mode 100644 analysis/thread-dump/src/main/java/org/eclipse/jifa/tda/vo/VBlockingThread.java create mode 100644 analysis/thread-dump/src/test/java/org/eclipse/jifa/tda/TestConverter.java create mode 100644 analysis/thread-dump/src/test/java/org/eclipse/jifa/tda/TestDiagnoser.java create mode 100644 analysis/thread-dump/src/test/resources/jstack_17_with_blocked.log create mode 100644 frontend/src/components/threaddump/BlockedThreads.vue create mode 100644 frontend/src/components/threaddump/CpuConsumingThreads.vue create mode 100644 frontend/src/components/threaddump/Diagnose.vue create mode 100644 frontend/src/components/threaddump/ThreadDumpOverview.vue create mode 100644 frontend/src/components/threaddump/ThreadDumpSearch.vue create mode 100644 frontend/src/components/threaddump/ThreadDumpSearchForm.vue diff --git a/NOTICE b/NOTICE index 575a1e90..bf8c72d5 100644 --- a/NOTICE +++ b/NOTICE @@ -175,3 +175,4 @@ permitted. Alibaba - Initial implementation Netflix - Basic customization hooks +SEEBURGER - ThreadDump enhancements diff --git a/analysis/thread-dump/src/main/java/org/eclipse/jifa/tda/ThreadDumpAnalyzer.java b/analysis/thread-dump/src/main/java/org/eclipse/jifa/tda/ThreadDumpAnalyzer.java index 22529a94..bd0e0463 100644 --- a/analysis/thread-dump/src/main/java/org/eclipse/jifa/tda/ThreadDumpAnalyzer.java +++ b/analysis/thread-dump/src/main/java/org/eclipse/jifa/tda/ThreadDumpAnalyzer.java @@ -21,6 +21,9 @@ import org.eclipse.jifa.common.domain.request.PagingRequest; import org.eclipse.jifa.common.domain.vo.PageView; import org.eclipse.jifa.common.util.PageViewBuilder; +import org.eclipse.jifa.tda.diagnoser.Diagnostic; +import org.eclipse.jifa.tda.diagnoser.ThreadDumpAnalysisConfig; +import org.eclipse.jifa.tda.diagnoser.ThreadDumpDiagnoser; import org.eclipse.jifa.tda.enums.MonitorState; import org.eclipse.jifa.tda.enums.ThreadType; import org.eclipse.jifa.tda.model.CallSiteTree; @@ -35,6 +38,8 @@ import org.eclipse.jifa.tda.util.CollectionUtil; import org.eclipse.jifa.tda.vo.Content; import org.eclipse.jifa.tda.vo.Overview; +import org.eclipse.jifa.tda.vo.SearchHit; +import org.eclipse.jifa.tda.vo.VBlockingThread; import org.eclipse.jifa.tda.vo.VFrame; import org.eclipse.jifa.tda.vo.VMonitor; import org.eclipse.jifa.tda.vo.VThread; @@ -44,10 +49,16 @@ import java.io.LineNumberReader; import java.nio.file.Path; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; +import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Map.Entry; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; /** * Thread dump analyzer @@ -176,13 +187,17 @@ private PageView buildVThreadPageView(List threads, PagingReque } /** - * @param name the thread name - * @param type the thread type - * @param paging paging request - * @return the threads filtered by name and type + * @param name the thread name filter (substring match, optional) + * @param type the thread type filter (optional) + * @param threadState the Java or OS thread state to filter by (optional) + * @param ids explicit list of thread ids to include (optional) + * @param paging paging request + * @return the threads filtered by name, type, state and/or id */ public PageView threads(@ApiParameterMeta(required = false) String name, @ApiParameterMeta(required = false) ThreadType type, + @ApiParameterMeta(required = false) String threadState, + @ApiParameterMeta(required = false) List ids, PagingRequest paging) { List threads = new ArrayList<>(); CollectionUtil.forEach(t -> { @@ -192,12 +207,29 @@ public PageView threads(@ApiParameterMeta(required = false) String name if (StringUtils.isNotBlank(name) && !t.getName().contains(name)) { return; } + if (StringUtils.isNotBlank(threadState) && !getThreadState(t).equals(threadState)) { + return; + } + if (ids != null && !ids.isEmpty() && !ids.contains(t.getId())) { + return; + } threads.add(t); }, snapshot.getJavaThreads(), snapshot.getNonJavaThreads()); return buildVThreadPageView(threads, paging); } + /** Returns the most specific state string for a thread (Java state if available, else OS state). */ + private String getThreadState(Thread t) { + if (t instanceof JavaThread) { + JavaThread jt = (JavaThread) t; + if (jt.getJavaThreadState() != null) { + return String.valueOf(jt.getJavaThreadState()); + } + } + return String.valueOf(t.getOsThreadState()); + } + /** * @param groupName the thread group name * @param paging paging request @@ -305,4 +337,256 @@ public Map threadCountsByMonitor(int id) { map.forEach((s, l) -> counts.put(s, l.size())); return counts; } + + /** + * Returns all threads that are blocking at least one other thread via monitor + * ownership, sorted descending by number of blocked threads, then by blocker + * thread name. + * + * @return list of blocking threads with their blocked threads and held monitor + */ + public List blockingThreads() { + List result = new ArrayList<>(); + + Map>> allMonitors = snapshot.getMonitorThreads(); + for (Entry>> monitorEntry : allMonitors.entrySet()) { + Map> monitorMap = monitorEntry.getValue(); + if (!monitorMap.containsKey(MonitorState.LOCKED)) { + continue; + } + Thread blockingThread = monitorMap.get(MonitorState.LOCKED).stream().findFirst().orElse(null); + List blockedThreads = new ArrayList<>(); + if (monitorMap.containsKey(MonitorState.WAITING_TO_LOCK)) { + blockedThreads.addAll(monitorMap.get(MonitorState.WAITING_TO_LOCK)); + } + if (monitorMap.containsKey(MonitorState.WAITING_TO_RE_LOCK)) { + blockedThreads.addAll(monitorMap.get(MonitorState.WAITING_TO_RE_LOCK)); + } + if (!blockedThreads.isEmpty() && blockingThread != null) { + VBlockingThread r = new VBlockingThread(); + r.setBlockedThreads(blockedThreads.stream() + .map(this::convertToVThread) + .collect(Collectors.toList())); + r.setBlockingThread(convertToVThread(blockingThread)); + Monitor mon = findBlockingMonitor(blockedThreads.get(0)); + if (mon != null) { + r.setHeldLock(new VMonitor( + mon.getRawMonitor().getId(), + mon.getRawMonitor().getAddress(), + mon.getRawMonitor().isClassInstance(), + mon.getRawMonitor().getClazz(), + mon.getState())); + } + result.add(r); + } + } + + result.sort(Comparator + .comparingInt(m -> m.getBlockedThreads().size()) + .reversed() + .thenComparing(m -> m.getBlockingThread().getName())); + return result; + } + + /** + * Returns the threads with the highest CPU usage, in descending order. + * + * @param type limit to threads of this type; {@code null} means all types + * @param max maximum number of results; {@code -1} means unlimited + * @return list of threads sorted from most to least CPU-intensive + */ + public List cpuConsumingThreads(@ApiParameterMeta(required = false) ThreadType type, + int max) { + Stream stream = snapshot.getThreadMap().values().stream() + .filter(t -> type == null || t.getType() == type); + stream = stream.sorted(Comparator.comparingDouble(Thread::getCpu).reversed()) + .limit(max < 0 ? Integer.MAX_VALUE : max); + return stream.map(this::convertToVThread).collect(Collectors.toList()); + } + + /** + * Computes which threads consumed the most CPU between two thread + * dumps by matching threads via their native thread id ({@code tid}). + * + * @param other the second (later) thread dump analyzer + * @param max maximum number of results; {@code -1} means unlimited + * @param type limit to threads of this type; {@code null} means all types + * @return threads sorted by delta-CPU descending + */ + public List cpuConsumingThreadsCompare(ThreadDumpAnalyzer other, int max, + @ApiParameterMeta(required = false) ThreadType type) { + int limit = max < 0 ? Integer.MAX_VALUE : max; + Map cpuDelta = new HashMap<>(); + for (Thread first : snapshot.getThreadMap().values()) { + if (type != null && first.getType() != type) { + continue; + } + Thread second = other.snapshot.getThreadMap().values().stream() + .filter(t -> t.getTid() == first.getTid()) + .findFirst().orElse(null); + if (second != null && second.getCpu() > 0) { + cpuDelta.put(first, second.getCpu() - first.getCpu()); + } + } + List result = new ArrayList<>(); + cpuDelta.entrySet().stream() + .sorted(Collections.reverseOrder(Map.Entry.comparingByValue())) + .limit(limit) + .forEach(e -> { + VThread vt = convertToVThread(e.getKey()); + vt.setCpu(e.getValue()); + result.add(vt); + }); + return result; + } + + /** + * Diagnoses the thread dump for potential issues based on the given + * configuration and returns any issues found. + * + * @param config the configuration to use; a default config is used if {@code null} + * @return potentially empty list of diagnostic issues + */ + public List diagnose( + @ApiParameterMeta(required = false) ThreadDumpAnalysisConfig config) { + return new ThreadDumpDiagnoser().analyze( + snapshot, + config != null ? config : new ThreadDumpAnalysisConfig()); + } + + /** + * Searches through all threads and returns those whose name, state or + * stack trace match all of the given terms. + * + * @param term search terms; each term must match at least one of the + * enabled search fields (AND semantics across terms) + * @param searchName include the thread name in the search (default: true) + * @param searchState include the thread state in the search (default: true) + * @param searchStack include the stack trace in the search (default: true) + * @param regex treat terms as regular expressions (default: false) + * @param matchCase perform a case-sensitive search (default: false) + * @param allowedJavaStates if non-empty, only include threads whose Java state is + * one of these values + * @return matching threads together with their raw content lines + */ + public List searchThreads( + @ApiParameterMeta(required = false) List term, + @ApiParameterMeta(required = false) Boolean searchName, + @ApiParameterMeta(required = false) Boolean searchState, + @ApiParameterMeta(required = false) Boolean searchStack, + @ApiParameterMeta(required = false) Boolean regex, + @ApiParameterMeta(required = false) Boolean matchCase, + @ApiParameterMeta(required = false) List allowedJavaStates) { + if (term == null || term.isEmpty()) { + return Collections.emptyList(); + } + + boolean doSearchName = !Boolean.FALSE.equals(searchName); + boolean doSearchState = !Boolean.FALSE.equals(searchState); + boolean doSearchStack = !Boolean.FALSE.equals(searchStack); + boolean doRegex = Boolean.TRUE.equals(regex); + int flags = Boolean.TRUE.equals(matchCase) ? 0 : Pattern.CASE_INSENSITIVE; + + List patterns = term.stream() + .map(t -> Pattern.compile(doRegex ? t : Pattern.quote(t), flags)) + .collect(Collectors.toList()); + + List results = new ArrayList<>(); + + CollectionUtil.forEach(t -> { + // Optional state pre-filter + if (allowedJavaStates != null && !allowedJavaStates.isEmpty()) { + if (!(t instanceof JavaThread)) return; + JavaThread jt = (JavaThread) t; + String state = jt.getJavaThreadState() != null + ? String.valueOf(jt.getJavaThreadState()) : ""; + if (!allowedJavaStates.contains(state)) return; + } + + List rawLines; + try { + rawLines = rawContentOfThread(t.getId()); + } catch (IOException e) { + rawLines = Collections.emptyList(); + } + + String nameStr = t.getName() != null ? t.getName() : ""; + String stateStr = getThreadState(t); + String stackStr = rawLines.size() > 1 + ? String.join("\n", rawLines.subList(1, rawLines.size())) + : ""; + + boolean matches = patterns.stream().allMatch(p -> + (doSearchName && p.matcher(nameStr).find()) + || (doSearchState && p.matcher(stateStr).find()) + || (doSearchStack && p.matcher(stackStr).find()) + ); + + if (matches) { + SearchHit hit = new SearchHit(); + hit.setId(t.getId()); + hit.setName(t.getName()); + hit.setOsState(String.valueOf(t.getOsThreadState())); + if (t instanceof JavaThread) { + JavaThread jt = (JavaThread) t; + if (jt.getJavaThreadState() != null) { + hit.setJavaState(String.valueOf(jt.getJavaThreadState())); + } + } + if (t.getCpu() > 0) hit.setCpu(t.getCpu()); + if (t.getElapsed() > 0) hit.setElapsed(t.getElapsed()); + hit.setLines(rawLines); + results.add(hit); + } + }, snapshot.getJavaThreads(), snapshot.getNonJavaThreads()); + + return results; + } + + // ------------------------------------------------------------------ + // private helpers + // ------------------------------------------------------------------ + + /** + * Converts a model {@link Thread} to a lightweight {@link VThread} VO, + * copying cpu and elapsed times when available ({@code > 0}). + */ + private VThread convertToVThread(Thread thread) { + VThread vt = new VThread(); + vt.setId(thread.getId()); + vt.setName(thread.getName()); + if (thread.getCpu() > 0) { + vt.setCpu(thread.getCpu()); + } + if (thread.getElapsed() > 0) { + vt.setElapsed(thread.getElapsed()); + } + return vt; + } + + /** + * Finds the monitor that a blocked thread is waiting to acquire by inspecting + * the thread-level monitor list and the first frame that carries monitor + * information. + */ + private Monitor findBlockingMonitor(Thread thread) { + List candidates = new ArrayList<>(); + if (thread instanceof JavaThread) { + JavaThread blockedThread = (JavaThread) thread; + if (blockedThread.getTrace() != null && blockedThread.getTrace().getFrames() != null) { + for (Frame frame : blockedThread.getTrace().getFrames()) { + if (frame.getMonitors() != null) { + Arrays.stream(frame.getMonitors()).forEach(candidates::add); + // only the first frame with monitors is relevant + break; + } + } + } + } + return candidates.stream() + .filter(m -> m.getState() == MonitorState.WAITING_TO_LOCK + || m.getState() == MonitorState.WAITING_TO_RE_LOCK) + .findFirst() + .orElse(null); + } } diff --git a/analysis/thread-dump/src/main/java/org/eclipse/jifa/tda/diagnoser/Diagnostic.java b/analysis/thread-dump/src/main/java/org/eclipse/jifa/tda/diagnoser/Diagnostic.java new file mode 100644 index 00000000..c76cfc40 --- /dev/null +++ b/analysis/thread-dump/src/main/java/org/eclipse/jifa/tda/diagnoser/Diagnostic.java @@ -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}. + *

+ * 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 params; + + /** Affected threads, may be {@code null} when not applicable. */ + private List 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 + } +} diff --git a/analysis/thread-dump/src/main/java/org/eclipse/jifa/tda/diagnoser/ThreadDumpAnalysisConfig.java b/analysis/thread-dump/src/main/java/org/eclipse/jifa/tda/diagnoser/ThreadDumpAnalysisConfig.java new file mode 100644 index 00000000..deb7a744 --- /dev/null +++ b/analysis/thread-dump/src/main/java/org/eclipse/jifa/tda/diagnoser/ThreadDumpAnalysisConfig.java @@ -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. + *

+ * 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; +} diff --git a/analysis/thread-dump/src/main/java/org/eclipse/jifa/tda/diagnoser/ThreadDumpDiagnoser.java b/analysis/thread-dump/src/main/java/org/eclipse/jifa/tda/diagnoser/ThreadDumpDiagnoser.java new file mode 100644 index 00000000..a15d7a91 --- /dev/null +++ b/analysis/thread-dump/src/main/java/org/eclipse/jifa/tda/diagnoser/ThreadDumpDiagnoser.java @@ -0,0 +1,208 @@ +/******************************************************************************** + * 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. + *

+ * 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 analyze(Snapshot snapshot, ThreadDumpAnalysisConfig config) { + List 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 results) { + if (snapshot.getDeadLockThreads() == null) { + return; + } + List 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 results) { + List 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 results) { + if (config.getHighThreadsThreshold() <= 0) { + return; + } + Collection 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 results) { + if (config.getHighStackSizeThreshold() <= 0) { + return; + } + List 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 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 results) { + if (config.getHighCpuConsumedRatio() <= 0) { + return; + } + List 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 results) { + if (!config.isReportThrowingException()) { + return; + } + List 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 toVThread(Collection 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 createParams(Collection threads) { + Map params = new HashMap<>(); + params.put(KEY_COUNT, threads.size()); + if (threads.size() == 1) { + params.put(KEY_NAME, threads.stream().findFirst().get().getName()); + } + return params; + } +} diff --git a/analysis/thread-dump/src/main/java/org/eclipse/jifa/tda/util/Converter.java b/analysis/thread-dump/src/main/java/org/eclipse/jifa/tda/util/Converter.java index 70e57295..692d90f3 100644 --- a/analysis/thread-dump/src/main/java/org/eclipse/jifa/tda/util/Converter.java +++ b/analysis/thread-dump/src/main/java/org/eclipse/jifa/tda/util/Converter.java @@ -13,18 +13,77 @@ package org.eclipse.jifa.tda.util; +import java.text.NumberFormat; +import java.text.ParseException; +import java.util.Locale; + public class Converter { + /** + * Converts a time string as produced by JVM thread dumps into milliseconds. + *

+ * Accepted unit suffixes are {@code ms} (milliseconds) and {@code s} (seconds). + * The numeric part is parsed via {@link #parseSecureDouble(String)}, which + * handles both dot ({@code '.'}) and comma ({@code ','}) decimal separators. + *

+     *   "0.50s"     →    500.0 ms  (standard/US)
+     *   "1,5s"      →   1500.0 ms  (German locale decimal comma)
+     *   "1.234,56s" → 1234560.0 ms  (European thousands + decimal comma)
+     *   "100ms"     →    100.0 ms
+     *   "100,5ms"   →    100.5 ms  (German locale)
+     * 
+ * + * @param str the time string from the thread dump + * @return the time in milliseconds, or {@code -1} if {@code str} is {@code null} + * @throws IllegalArgumentException if the string has an unrecognised unit suffix + */ public static double str2TimeMillis(String str) { if (str == null) { return -1; } int length = str.length(); if (str.endsWith("ms")) { - return Double.parseDouble(str.substring(0, length - 2)); + return parseSecureDouble(str.substring(0, length - 2)); } else if (str.endsWith("s")) { - return Double.parseDouble(str.substring(0, length - 1)) * 1000; + return parseSecureDouble(str.substring(0, length - 1)) * 1000; } throw new IllegalArgumentException(str); } + + /** + * Parses a numeric string that may use either {@code '.'} or {@code ','} as + * the decimal separator, using a safe two-step strategy: + *
    + *
  1. Attempt {@link Double#parseDouble(String)} (US / standard notation, + * e.g. {@code "1.5"}, {@code "100"}).
  2. + *
  3. On {@link NumberFormatException}, fall back to + * {@link NumberFormat#getInstance(Locale) NumberFormat.getInstance(Locale.GERMANY)}, + * which correctly handles the German/European decimal comma and + * dot-as-thousands-separator + * (e.g. {@code "1,5"} → {@code 1.5}, + * {@code "1.234,56"} → {@code 1234.56}).
  4. + *
+ *

+ * This two-step approach avoids the ambiguity of position-based heuristics. + * A string such as {@code "1,234"} (single comma only) is unambiguously + * handled: standard parsing fails, German-locale parsing interprets the comma + * as a decimal separator, yielding {@code 1.234} – the semantically correct + * value for a time produced by a JVM running in a German locale. + * + * @param s the numeric string to parse; leading/trailing whitespace is trimmed + * @return the parsed {@code double} value + * @throws IllegalArgumentException if {@code s} cannot be parsed by either strategy + */ + public static double parseSecureDouble(String s) { + String clean = s.trim(); + try { + return Double.parseDouble(clean); + } catch (NumberFormatException e) { + try { + return NumberFormat.getInstance(Locale.GERMANY).parse(clean).doubleValue(); + } catch (ParseException pe) { + throw new IllegalArgumentException("Cannot parse '" + s + "' as a number", pe); + } + } + } } diff --git a/analysis/thread-dump/src/main/java/org/eclipse/jifa/tda/vo/SearchHit.java b/analysis/thread-dump/src/main/java/org/eclipse/jifa/tda/vo/SearchHit.java new file mode 100644 index 00000000..a68b6117 --- /dev/null +++ b/analysis/thread-dump/src/main/java/org/eclipse/jifa/tda/vo/SearchHit.java @@ -0,0 +1,55 @@ +/******************************************************************************** + * 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.vo; + +import lombok.Data; + +import java.util.List; + +/** + * A single search result containing a thread that matched the search query, + * along with the raw content lines of that thread's stack trace. + */ +@Data +public class SearchHit { + + /** Internal thread id (maps to thread detail view). */ + private int id; + + /** Display name of the thread. */ + private String name; + + /** Java thread state string (may be null for non-Java threads). */ + private String javaState; + + /** OS thread state string. */ + private String osState; + + /** CPU time in milliseconds (0 if not available). */ + private double cpu; + + /** Elapsed time in milliseconds (0 if not available). */ + private double elapsed; + + /** + * Raw content lines of the thread's stack trace entry. + * Line 0 is the thread header; subsequent lines are stack frames. + */ + private List lines; +} diff --git a/analysis/thread-dump/src/main/java/org/eclipse/jifa/tda/vo/VBlockingThread.java b/analysis/thread-dump/src/main/java/org/eclipse/jifa/tda/vo/VBlockingThread.java new file mode 100644 index 00000000..41f79dda --- /dev/null +++ b/analysis/thread-dump/src/main/java/org/eclipse/jifa/tda/vo/VBlockingThread.java @@ -0,0 +1,40 @@ +/******************************************************************************** + * 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.vo; + +import lombok.Data; + +import java.util.List; + +/** + * Value object representing a thread that is blocking one or more other threads + * via monitor ownership. + */ +@Data +public class VBlockingThread { + + /** The thread that holds the monitor. */ + private VThread blockingThread; + + /** The threads waiting to acquire the monitor held by {@link #blockingThread}. */ + private List blockedThreads; + + /** The monitor that is the source of the contention, or {@code null} if unknown. */ + private VMonitor heldLock; +} diff --git a/analysis/thread-dump/src/main/java/org/eclipse/jifa/tda/vo/VThread.java b/analysis/thread-dump/src/main/java/org/eclipse/jifa/tda/vo/VThread.java index 94bde49e..8f461fe6 100644 --- a/analysis/thread-dump/src/main/java/org/eclipse/jifa/tda/vo/VThread.java +++ b/analysis/thread-dump/src/main/java/org/eclipse/jifa/tda/vo/VThread.java @@ -1,5 +1,5 @@ /******************************************************************************** - * Copyright (c) 2022 Contributors to the Eclipse Foundation + * Copyright (c) 2022, 2023 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. @@ -13,12 +13,26 @@ package org.eclipse.jifa.tda.vo; +import lombok.AllArgsConstructor; import lombok.Data; +import lombok.NoArgsConstructor; @Data +@NoArgsConstructor +@AllArgsConstructor public class VThread { private int id; private String name; + + /** + * CPU time in milliseconds; {@code null} if unknown / not available. + */ + private Double cpu; + + /** + * Elapsed (wall-clock) time in milliseconds; {@code null} if unknown. + */ + private Double elapsed; } diff --git a/analysis/thread-dump/src/test/java/org/eclipse/jifa/tda/TestAnalyzer.java b/analysis/thread-dump/src/test/java/org/eclipse/jifa/tda/TestAnalyzer.java index 247b842f..77ef09a0 100644 --- a/analysis/thread-dump/src/test/java/org/eclipse/jifa/tda/TestAnalyzer.java +++ b/analysis/thread-dump/src/test/java/org/eclipse/jifa/tda/TestAnalyzer.java @@ -36,7 +36,7 @@ public void test() throws Exception { Assertions.assertEquals(o1, o2); Assertions.assertEquals(o1.hashCode(), o2.hashCode()); - PageView threads = tda.threads("main", ThreadType.JAVA, new PagingRequest(1, 1)); + PageView threads = tda.threads("main", ThreadType.JAVA, null, null, new PagingRequest(1, 1)); Assertions.assertEquals(1, threads.getTotalSize()); PageView frames = tda.callSiteTree(0, new PagingRequest(1, 16)); diff --git a/analysis/thread-dump/src/test/java/org/eclipse/jifa/tda/TestConverter.java b/analysis/thread-dump/src/test/java/org/eclipse/jifa/tda/TestConverter.java new file mode 100644 index 00000000..2e64d9be --- /dev/null +++ b/analysis/thread-dump/src/test/java/org/eclipse/jifa/tda/TestConverter.java @@ -0,0 +1,118 @@ +/******************************************************************************** + * 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; + +import org.eclipse.jifa.tda.util.Converter; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class TestConverter { + + // ----------------------------------------------------------------- + // parseSecureDouble – unit tests for the helper directly + // ----------------------------------------------------------------- + + @Test + public void testParseSecureDouble_standardDot() { + assertEquals(1.5, Converter.parseSecureDouble("1.5"), 0.001); + assertEquals(0.5, Converter.parseSecureDouble("0.5"), 0.001); + assertEquals(100.0, Converter.parseSecureDouble("100"), 0.001); + assertEquals(1234.56, Converter.parseSecureDouble("1234.56"), 0.001); + } + + @Test + public void testParseSecureDouble_germanDecimalComma() { + // Single comma → decimal separator (German locale fallback) + assertEquals(1.5, Converter.parseSecureDouble("1,5"), 0.001); + assertEquals(0.5, Converter.parseSecureDouble("0,5"), 0.001); + assertEquals(0.50, Converter.parseSecureDouble("0,50"), 0.001); + } + + @Test + public void testParseSecureDouble_ambiguousSingleComma() { + // "1,234" – standard parse fails, German locale treats comma as decimal → 1.234 + // (correct for a JVM running in German locale; 1234 is not a valid time value here) + assertEquals(1.234, Converter.parseSecureDouble("1,234"), 0.001); + } + + @Test + public void testParseSecureDouble_europeanThousandsAndDecimal() { + // "1.234,56" – standard parse fails, German locale → 1234.56 + assertEquals(1234.56, Converter.parseSecureDouble("1.234,56"), 0.001); + assertEquals(1000.0, Converter.parseSecureDouble("1.000,0"), 0.001); + } + + @Test + public void testParseSecureDouble_whitespace() { + assertEquals(1.5, Converter.parseSecureDouble(" 1.5 "), 0.001); + assertEquals(1.5, Converter.parseSecureDouble(" 1,5 "), 0.001); + } + + @Test + public void testParseSecureDouble_invalid() { + assertThrows(IllegalArgumentException.class, () -> Converter.parseSecureDouble("abc")); + assertThrows(IllegalArgumentException.class, () -> Converter.parseSecureDouble("")); + } + + // ----------------------------------------------------------------- + // str2TimeMillis – end-to-end tests + // ----------------------------------------------------------------- + + @Test + public void testNull() { + assertEquals(-1.0, Converter.str2TimeMillis(null)); + } + + @Test + public void testMilliseconds_dot() { + assertEquals(100.0, Converter.str2TimeMillis("100ms"), 0.001); + assertEquals(100.5, Converter.str2TimeMillis("100.5ms"), 0.001); + } + + @Test + public void testMilliseconds_germanComma() { + assertEquals(100.5, Converter.str2TimeMillis("100,5ms"), 0.001); + } + + @Test + public void testSeconds_dot() { + assertEquals(500.0, Converter.str2TimeMillis("0.5s"), 0.001); + assertEquals(1000.0, Converter.str2TimeMillis("1s"), 0.001); + assertEquals(1500.0, Converter.str2TimeMillis("1.5s"), 0.001); + } + + @Test + public void testSeconds_germanComma() { + assertEquals(1500.0, Converter.str2TimeMillis("1,5s"), 0.001); + assertEquals(500.0, Converter.str2TimeMillis("0,5s"), 0.001); + } + + @Test + public void testSeconds_europeanThousandsAndDecimal() { + // "1.234,56s" → German locale → 1234.56 s = 1_234_560 ms + assertEquals(1234560.0, Converter.str2TimeMillis("1.234,56s"), 0.001); + } + + @Test + public void testUnknownFormat() { + assertThrows(IllegalArgumentException.class, () -> Converter.str2TimeMillis("1.5x")); + assertThrows(IllegalArgumentException.class, () -> Converter.str2TimeMillis("abc")); + } +} diff --git a/analysis/thread-dump/src/test/java/org/eclipse/jifa/tda/TestDiagnoser.java b/analysis/thread-dump/src/test/java/org/eclipse/jifa/tda/TestDiagnoser.java new file mode 100644 index 00000000..edd972e8 --- /dev/null +++ b/analysis/thread-dump/src/test/java/org/eclipse/jifa/tda/TestDiagnoser.java @@ -0,0 +1,170 @@ +/******************************************************************************** + * 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; + +import org.eclipse.jifa.analysis.listener.DefaultProgressListener; +import org.eclipse.jifa.tda.diagnoser.Diagnostic; +import org.eclipse.jifa.tda.diagnoser.Diagnostic.Severity; +import org.eclipse.jifa.tda.diagnoser.Diagnostic.Type; +import org.eclipse.jifa.tda.diagnoser.ThreadDumpAnalysisConfig; +import org.eclipse.jifa.tda.vo.VBlockingThread; +import org.eclipse.jifa.tda.vo.VThread; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Tests for the new Phase-2 analyzer features: + * blockingThreads(), cpuConsumingThreads(), analyze() / ThreadDumpDiagnoser. + *

+ * Uses jstack_17_with_blocked.log which contains: + * - German-locale decimal commas in cpu/elapsed (e.g. "360,22ms", "0,74s") + * - 1 blocking thread (pool-1-thread-2) holding a monitor + * - 4 blocked threads (pool-1-thread-1/3/4/5) waiting on that monitor + */ +public class TestDiagnoser extends TestBase { + + private ThreadDumpAnalyzer tda; + + @BeforeEach + void setup() throws Exception { + tda = new ThreadDumpAnalyzer( + pathOfResource("jstack_17_with_blocked.log"), + new DefaultProgressListener()); + } + + // ------------------------------------------------------------------ + // blockingThreads() + // ------------------------------------------------------------------ + + @Test + public void testBlockingThreads_count() { + List blocking = tda.blockingThreads(); + // pool-1-thread-2 holds the lock; 4 threads are blocked on it + assertEquals(1, blocking.size()); + } + + @Test + public void testBlockingThreads_blockerName() { + VBlockingThread bt = tda.blockingThreads().get(0); + assertEquals("pool-1-thread-2", bt.getBlockingThread().getName()); + } + + @Test + public void testBlockingThreads_blockedCount() { + VBlockingThread bt = tda.blockingThreads().get(0); + assertEquals(4, bt.getBlockedThreads().size()); + } + + @Test + public void testBlockingThreads_heldLockPresent() { + VBlockingThread bt = tda.blockingThreads().get(0); + assertNotNull(bt.getHeldLock()); + } + + // ------------------------------------------------------------------ + // cpuConsumingThreads() – also validates German-locale decimal parsing + // ------------------------------------------------------------------ + + @Test + public void testCpuConsumingThreads_parsesGermanLocale() { + // "main" has cpu=360,22ms in the log – Converter must parse the comma + List threads = tda.cpuConsumingThreads(null, 1); + assertEquals(1, threads.size()); + assertEquals("main", threads.get(0).getName()); + assertNotNull(threads.get(0).getCpu()); + assertTrue(threads.get(0).getCpu() > 0, + "CPU time should be > 0 after parsing German-locale comma"); + } + + @Test + public void testCpuConsumingThreads_limitRespected() { + List top3 = tda.cpuConsumingThreads(null, 3); + assertTrue(top3.size() <= 3); + } + + @Test + public void testCpuConsumingThreads_sortedDescending() { + List threads = tda.cpuConsumingThreads(null, -1); + for (int i = 0; i < threads.size() - 1; i++) { + double a = threads.get(i).getCpu() != null ? threads.get(i).getCpu() : 0; + double b = threads.get(i + 1).getCpu() != null ? threads.get(i + 1).getCpu() : 0; + assertTrue(a >= b, "List must be sorted descending by CPU"); + } + } + + // ------------------------------------------------------------------ + // analyze() / ThreadDumpDiagnoser + // ------------------------------------------------------------------ + + @Test + public void testAnalyze_defaultConfig_findsBlockedThreads() { + // Default threshold is 3; the dump has 4 blocked threads → should fire + List diagnostics = tda.diagnose(null); + assertTrue(diagnostics.stream() + .anyMatch(d -> d.getType() == Type.HIGH_BLOCKED_THREAD_COUNT), + "Expected HIGH_BLOCKED_THREAD_COUNT diagnostic"); + } + + @Test + public void testAnalyze_blockedDiagnosticSeverity() { + List diagnostics = tda.diagnose(null); + Diagnostic d = diagnostics.stream() + .filter(x -> x.getType() == Type.HIGH_BLOCKED_THREAD_COUNT) + .findFirst().orElseThrow(); + assertEquals(Severity.ERROR, d.getSeverity()); + } + + @Test + public void testAnalyze_blockedDiagnosticParams() { + List diagnostics = tda.diagnose(null); + Diagnostic d = diagnostics.stream() + .filter(x -> x.getType() == Type.HIGH_BLOCKED_THREAD_COUNT) + .findFirst().orElseThrow(); + assertEquals(4, d.getParams().get("count")); + } + + @Test + public void testAnalyze_raisedThreshold_noBlockedDiagnostic() { + ThreadDumpAnalysisConfig config = new ThreadDumpAnalysisConfig(); + config.setHighBlockedThreadsThreshold(10); // higher than 4 blocked threads + List diagnostics = tda.diagnose(config); + assertTrue(diagnostics.stream() + .noneMatch(d -> d.getType() == Type.HIGH_BLOCKED_THREAD_COUNT), + "No HIGH_BLOCKED_THREAD_COUNT expected when threshold is raised"); + } + + @Test + public void testAnalyze_disabledCheck_returnsEmpty() { + ThreadDumpAnalysisConfig config = new ThreadDumpAnalysisConfig(); + // Use very high thresholds so no check fires on this small dump, + // and disable exception/CPU checks explicitly + config.setHighBlockedThreadsThreshold(Integer.MAX_VALUE); + config.setHighThreadsThreshold(Integer.MAX_VALUE); + config.setHighStackSizeThreshold(Integer.MAX_VALUE); + config.setHighCpuConsumedRatio(2.0); // ratio > 1.0 is impossible + config.setReportThrowingException(false); + List diagnostics = tda.diagnose(config); + assertTrue(diagnostics.isEmpty(), + "All checks suppressed – list must be empty, but got: " + diagnostics); + } +} diff --git a/analysis/thread-dump/src/test/resources/jstack_17_with_blocked.log b/analysis/thread-dump/src/test/resources/jstack_17_with_blocked.log new file mode 100644 index 00000000..39aedfd4 --- /dev/null +++ b/analysis/thread-dump/src/test/resources/jstack_17_with_blocked.log @@ -0,0 +1,146 @@ +2023-09-07 21:19:13 +Full thread dump OpenJDK 64-Bit Server VM (17.0.7+7 mixed mode): + +Threads class SMR info: +_java_thread_list=0x00007fb5ac001fa0, length=18, elements={ +0x00007fb5f402bf30, 0x00007fb5f418e5e0, 0x00007fb5f418fc50, 0x00007fb5f41b4e40, +0x00007fb5f41b6210, 0x00007fb5f41b7640, 0x00007fb5f41b8ce0, 0x00007fb5f41ba1e0, +0x00007fb5f41bb670, 0x00007fb5f4274c50, 0x00007fb5f427c600, 0x00007fb5f42aa730, +0x00007fb5f42ab5b0, 0x00007fb5f42ac5d0, 0x00007fb5f42ad5f0, 0x00007fb5f42ae9f0, +0x00007fb5f4379390, 0x00007fb5ac000ff0 +} + +"main" #1 prio=5 os_prio=0 cpu=360,22ms elapsed=0,74s tid=0x00007fb5f402bf30 nid=0x44c8 in Object.wait() [0x00007fb5fad87000] + java.lang.Thread.State: WAITING (on object monitor) + at java.lang.Object.wait(java.base@17.0.7/Native Method) + - waiting on <0x000000008d7731e8> (a java.lang.ProcessImpl) + at java.lang.Object.wait(java.base@17.0.7/Object.java:338) + at java.lang.ProcessImpl.waitFor(java.base@17.0.7/ProcessImpl.java:434) + - locked <0x000000008d7731e8> (a java.lang.ProcessImpl) + at scratchpad.Dumper.main(Dumper.java:30) + +"Reference Handler" #2 daemon prio=10 os_prio=0 cpu=0,43ms elapsed=0,62s tid=0x00007fb5f418e5e0 nid=0x44cf waiting on condition [0x00007fb5f8da2000] + java.lang.Thread.State: RUNNABLE + at java.lang.ref.Reference.waitForReferencePendingList(java.base@17.0.7/Native Method) + at java.lang.ref.Reference.processPendingReferences(java.base@17.0.7/Reference.java:253) + at java.lang.ref.Reference$ReferenceHandler.run(java.base@17.0.7/Reference.java:215) + +"Finalizer" #3 daemon prio=8 os_prio=0 cpu=1,28ms elapsed=0,62s tid=0x00007fb5f418fc50 nid=0x44d0 in Object.wait() [0x00007fb5f8ca2000] + java.lang.Thread.State: WAITING (on object monitor) + at java.lang.Object.wait(java.base@17.0.7/Native Method) + - waiting on <0x000000008d90bff0> (a java.lang.ref.ReferenceQueue$Lock) + at java.lang.ref.ReferenceQueue.remove(java.base@17.0.7/ReferenceQueue.java:155) + - locked <0x000000008d90bff0> (a java.lang.ref.ReferenceQueue$Lock) + at java.lang.ref.ReferenceQueue.remove(java.base@17.0.7/ReferenceQueue.java:176) + at java.lang.ref.Finalizer$FinalizerThread.run(java.base@17.0.7/Finalizer.java:172) + +"Signal Dispatcher" #4 daemon prio=9 os_prio=0 cpu=0,52ms elapsed=0,58s tid=0x00007fb5f41b4e40 nid=0x44d1 waiting on condition [0x0000000000000000] + java.lang.Thread.State: RUNNABLE + +"Service Thread" #5 daemon prio=9 os_prio=0 cpu=0,21ms elapsed=0,58s tid=0x00007fb5f41b6210 nid=0x44d2 runnable [0x0000000000000000] + java.lang.Thread.State: RUNNABLE + +"Monitor Deflation Thread" #6 daemon prio=9 os_prio=0 cpu=0,17ms elapsed=0,58s tid=0x00007fb5f41b7640 nid=0x44d3 runnable [0x0000000000000000] + java.lang.Thread.State: RUNNABLE + +"C2 CompilerThread0" #7 daemon prio=9 os_prio=0 cpu=37,63ms elapsed=0,58s tid=0x00007fb5f41b8ce0 nid=0x44d4 waiting on condition [0x0000000000000000] + java.lang.Thread.State: RUNNABLE + No compile task + +"C1 CompilerThread0" #9 daemon prio=9 os_prio=0 cpu=72,58ms elapsed=0,58s tid=0x00007fb5f41ba1e0 nid=0x44d5 waiting on condition [0x0000000000000000] + java.lang.Thread.State: RUNNABLE + No compile task + +"Sweeper thread" #10 daemon prio=9 os_prio=0 cpu=0,22ms elapsed=0,58s tid=0x00007fb5f41bb670 nid=0x44d6 runnable [0x0000000000000000] + java.lang.Thread.State: RUNNABLE + +"Notification Thread" #11 daemon prio=9 os_prio=0 cpu=0,12ms elapsed=0,53s tid=0x00007fb5f4274c50 nid=0x44d7 runnable [0x0000000000000000] + java.lang.Thread.State: RUNNABLE + +"Common-Cleaner" #12 daemon prio=8 os_prio=0 cpu=0,22ms elapsed=0,52s tid=0x00007fb5f427c600 nid=0x44d9 in Object.wait() [0x00007fb5dd997000] + java.lang.Thread.State: TIMED_WAITING (on object monitor) + at java.lang.Object.wait(java.base@17.0.7/Native Method) + - waiting on <0x000000008d9c2438> (a java.lang.ref.ReferenceQueue$Lock) + at java.lang.ref.ReferenceQueue.remove(java.base@17.0.7/ReferenceQueue.java:155) + - locked <0x000000008d9c2438> (a java.lang.ref.ReferenceQueue$Lock) + at jdk.internal.ref.CleanerImpl.run(java.base@17.0.7/CleanerImpl.java:140) + at java.lang.Thread.run(java.base@17.0.7/Thread.java:833) + at jdk.internal.misc.InnocuousThread.run(java.base@17.0.7/InnocuousThread.java:162) + +"pool-1-thread-1" #13 prio=5 os_prio=0 cpu=0,64ms elapsed=0,50s tid=0x00007fb5f42aa730 nid=0x44da waiting for monitor entry [0x00007fb5dd897000] + java.lang.Thread.State: BLOCKED (on object monitor) + at scratchpad.Dumper.lambda$0(Dumper.java:22) + - waiting to lock <0x000000008d9c6d38> (a java.lang.Object) + at scratchpad.Dumper$$Lambda$1/0x0000000100000a08.call(Unknown Source) + at java.util.concurrent.FutureTask.run(java.base@17.0.7/FutureTask.java:264) + at java.util.concurrent.ThreadPoolExecutor.runWorker(java.base@17.0.7/ThreadPoolExecutor.java:1136) + at java.util.concurrent.ThreadPoolExecutor$Worker.run(java.base@17.0.7/ThreadPoolExecutor.java:635) + at java.lang.Thread.run(java.base@17.0.7/Thread.java:833) + +"pool-1-thread-2" #14 prio=5 os_prio=0 cpu=0,57ms elapsed=0,50s tid=0x00007fb5f42ab5b0 nid=0x44db waiting on condition [0x00007fb5dd797000] + java.lang.Thread.State: TIMED_WAITING (sleeping) + at java.lang.Thread.sleep(java.base@17.0.7/Native Method) + at scratchpad.Dumper.lambda$0(Dumper.java:22) + - locked <0x000000008d9c6d38> (a java.lang.Object) + at scratchpad.Dumper$$Lambda$1/0x0000000100000a08.call(Unknown Source) + at java.util.concurrent.FutureTask.run(java.base@17.0.7/FutureTask.java:264) + at java.util.concurrent.ThreadPoolExecutor.runWorker(java.base@17.0.7/ThreadPoolExecutor.java:1136) + at java.util.concurrent.ThreadPoolExecutor$Worker.run(java.base@17.0.7/ThreadPoolExecutor.java:635) + at java.lang.Thread.run(java.base@17.0.7/Thread.java:833) + +"pool-1-thread-3" #15 prio=5 os_prio=0 cpu=0,23ms elapsed=0,49s tid=0x00007fb5f42ac5d0 nid=0x44dc waiting for monitor entry [0x00007fb5dd697000] + java.lang.Thread.State: BLOCKED (on object monitor) + at scratchpad.Dumper.lambda$0(Dumper.java:22) + - waiting to lock <0x000000008d9c6d38> (a java.lang.Object) + at scratchpad.Dumper$$Lambda$1/0x0000000100000a08.call(Unknown Source) + at java.util.concurrent.FutureTask.run(java.base@17.0.7/FutureTask.java:264) + at java.util.concurrent.ThreadPoolExecutor.runWorker(java.base@17.0.7/ThreadPoolExecutor.java:1136) + at java.util.concurrent.ThreadPoolExecutor$Worker.run(java.base@17.0.7/ThreadPoolExecutor.java:635) + at java.lang.Thread.run(java.base@17.0.7/Thread.java:833) + +"pool-1-thread-4" #16 prio=5 os_prio=0 cpu=0,23ms elapsed=0,49s tid=0x00007fb5f42ad5f0 nid=0x44dd waiting for monitor entry [0x00007fb5dd597000] + java.lang.Thread.State: BLOCKED (on object monitor) + at scratchpad.Dumper.lambda$0(Dumper.java:22) + - waiting to lock <0x000000008d9c6d38> (a java.lang.Object) + at scratchpad.Dumper$$Lambda$1/0x0000000100000a08.call(Unknown Source) + at java.util.concurrent.FutureTask.run(java.base@17.0.7/FutureTask.java:264) + at java.util.concurrent.ThreadPoolExecutor.runWorker(java.base@17.0.7/ThreadPoolExecutor.java:1136) + at java.util.concurrent.ThreadPoolExecutor$Worker.run(java.base@17.0.7/ThreadPoolExecutor.java:635) + at java.lang.Thread.run(java.base@17.0.7/Thread.java:833) + +"pool-1-thread-5" #17 prio=5 os_prio=0 cpu=0,12ms elapsed=0,49s tid=0x00007fb5f42ae9f0 nid=0x44de waiting for monitor entry [0x00007fb5dd497000] + java.lang.Thread.State: BLOCKED (on object monitor) + at scratchpad.Dumper.lambda$0(Dumper.java:22) + - waiting to lock <0x000000008d9c6d38> (a java.lang.Object) + at scratchpad.Dumper$$Lambda$1/0x0000000100000a08.call(Unknown Source) + at java.util.concurrent.FutureTask.run(java.base@17.0.7/FutureTask.java:264) + at java.util.concurrent.ThreadPoolExecutor.runWorker(java.base@17.0.7/ThreadPoolExecutor.java:1136) + at java.util.concurrent.ThreadPoolExecutor$Worker.run(java.base@17.0.7/ThreadPoolExecutor.java:635) + at java.lang.Thread.run(java.base@17.0.7/Thread.java:833) + +"process reaper (pid 17631)" #18 daemon prio=10 os_prio=0 cpu=0,35ms elapsed=0,39s tid=0x00007fb5f4379390 nid=0x44e6 runnable [0x00007fb5f8061000] + java.lang.Thread.State: RUNNABLE + at java.lang.ProcessHandleImpl.waitForProcessExit0(java.base@17.0.7/Native Method) + at java.lang.ProcessHandleImpl$1.run(java.base@17.0.7/ProcessHandleImpl.java:150) + at java.util.concurrent.ThreadPoolExecutor.runWorker(java.base@17.0.7/ThreadPoolExecutor.java:1136) + at java.util.concurrent.ThreadPoolExecutor$Worker.run(java.base@17.0.7/ThreadPoolExecutor.java:635) + at java.lang.Thread.run(java.base@17.0.7/Thread.java:833) + +"Attach Listener" #19 daemon prio=9 os_prio=0 cpu=0,34ms elapsed=0,10s tid=0x00007fb5ac000ff0 nid=0x44f2 waiting on condition [0x0000000000000000] + java.lang.Thread.State: RUNNABLE + +"VM Thread" os_prio=0 cpu=0,61ms elapsed=0,65s tid=0x00007fb5f413eed0 nid=0x44ce runnable + +"GC Thread#0" os_prio=0 cpu=0,30ms elapsed=0,74s tid=0x00007fb5f4084500 nid=0x44c9 runnable + +"G1 Main Marker" os_prio=0 cpu=0,19ms elapsed=0,74s tid=0x00007fb5f4094f00 nid=0x44ca runnable + +"G1 Conc#0" os_prio=0 cpu=0,12ms elapsed=0,74s tid=0x00007fb5f4095e80 nid=0x44cb runnable + +"G1 Refine#0" os_prio=0 cpu=0,20ms elapsed=0,74s tid=0x00007fb5f40f9050 nid=0x44cc runnable + +"G1 Service" os_prio=0 cpu=0,26ms elapsed=0,74s tid=0x00007fb5f40f9f60 nid=0x44cd runnable + +"VM Periodic Task Thread" os_prio=0 cpu=0,34ms elapsed=0,53s tid=0x00007fb5f41e1030 nid=0x44d8 waiting on condition + +JNI global refs: 11, weak refs: 0 diff --git a/frontend/components.d.ts b/frontend/components.d.ts index 412dee7a..d026d51e 100644 --- a/frontend/components.d.ts +++ b/frontend/components.d.ts @@ -13,6 +13,7 @@ declare module 'vue' { ElButton: typeof import('element-plus/es')['ElButton'] ElCard: typeof import('element-plus/es')['ElCard'] ElCheckbox: typeof import('element-plus/es')['ElCheckbox'] + ElCol: typeof import('element-plus/es')['ElCol'] ElCollapse: typeof import('element-plus/es')['ElCollapse'] ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem'] ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider'] @@ -37,6 +38,7 @@ declare module 'vue' { ElRadioButton: typeof import('element-plus/es')['ElRadioButton'] ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup'] ElResult: typeof import('element-plus/es')['ElResult'] + ElRow: typeof import('element-plus/es')['ElRow'] ElScrollbar: typeof import('element-plus/es')['ElScrollbar'] ElSelect: typeof import('element-plus/es')['ElSelect'] ElSpace: typeof import('element-plus/es')['ElSpace'] diff --git a/frontend/package-lock.json b/frontend/package-lock.json index b17e869b..65def77e 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -12,6 +12,7 @@ "axios": "^1.8.2", "axios-retry": "^3.8.0", "chart.js": "^4.4.0", + "d3": "^7.9.0", "echarts": "^5.4.3", "element-plus": "^2.6.0", "js-base64": "^3.7.5", @@ -3129,6 +3130,416 @@ "type": "^1.0.1" } }, + "node_modules/d3": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz", + "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==", + "license": "ISC", + "dependencies": { + "d3-array": "3", + "d3-axis": "3", + "d3-brush": "3", + "d3-chord": "3", + "d3-color": "3", + "d3-contour": "4", + "d3-delaunay": "6", + "d3-dispatch": "3", + "d3-drag": "3", + "d3-dsv": "3", + "d3-ease": "3", + "d3-fetch": "3", + "d3-force": "3", + "d3-format": "3", + "d3-geo": "3", + "d3-hierarchy": "3", + "d3-interpolate": "3", + "d3-path": "3", + "d3-polygon": "3", + "d3-quadtree": "3", + "d3-random": "3", + "d3-scale": "4", + "d3-scale-chromatic": "3", + "d3-selection": "3", + "d3-shape": "3", + "d3-time": "3", + "d3-time-format": "4", + "d3-timer": "3", + "d3-transition": "3", + "d3-zoom": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-axis": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-brush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "3", + "d3-transition": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-chord": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", + "license": "ISC", + "dependencies": { + "d3-path": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-contour": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", + "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", + "license": "ISC", + "dependencies": { + "d3-array": "^3.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", + "license": "ISC", + "dependencies": { + "delaunator": "5" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "license": "ISC", + "dependencies": { + "commander": "7", + "iconv-lite": "0.6", + "rw": "1" + }, + "bin": { + "csv2json": "bin/dsv2json.js", + "csv2tsv": "bin/dsv2dsv.js", + "dsv2dsv": "bin/dsv2dsv.js", + "dsv2json": "bin/dsv2json.js", + "json2csv": "bin/json2dsv.js", + "json2dsv": "bin/json2dsv.js", + "json2tsv": "bin/json2dsv.js", + "tsv2csv": "bin/dsv2dsv.js", + "tsv2json": "bin/dsv2json.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-fetch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", + "license": "ISC", + "dependencies": { + "d3-dsv": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-force": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-geo": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz", + "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2.5.0 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-hierarchy": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-polygon": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-quadtree": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-random": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/data-urls": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-4.0.0.tgz", @@ -3257,6 +3668,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delaunator": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.1.0.tgz", + "integrity": "sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==", + "license": "ISC", + "dependencies": { + "robust-predicates": "^3.0.2" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -4796,7 +5216,6 @@ "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" }, @@ -4879,6 +5298,15 @@ "node": ">= 0.4" } }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/is-array-buffer": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", @@ -6472,6 +6900,12 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/robust-predicates": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.3.tgz", + "integrity": "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==", + "license": "Unlicense" + }, "node_modules/rollup": { "version": "3.29.5", "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.5.tgz", @@ -6623,6 +7057,12 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", + "license": "BSD-3-Clause" + }, "node_modules/safe-array-concat": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.0.0.tgz", @@ -6658,8 +7098,7 @@ "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "node_modules/sass": { "version": "1.65.1", diff --git a/frontend/package.json b/frontend/package.json index 74589996..a09dc26d 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -18,6 +18,7 @@ "axios": "^1.8.2", "axios-retry": "^3.8.0", "chart.js": "^4.4.0", + "d3": "^7.9.0", "echarts": "^5.4.3", "element-plus": "^2.6.0", "js-base64": "^3.7.5", diff --git a/frontend/src/components/threaddump/BlockedThreads.vue b/frontend/src/components/threaddump/BlockedThreads.vue new file mode 100644 index 00000000..17698ac3 --- /dev/null +++ b/frontend/src/components/threaddump/BlockedThreads.vue @@ -0,0 +1,214 @@ + + + + + + diff --git a/frontend/src/components/threaddump/CpuConsumingThreads.vue b/frontend/src/components/threaddump/CpuConsumingThreads.vue new file mode 100644 index 00000000..49e963a6 --- /dev/null +++ b/frontend/src/components/threaddump/CpuConsumingThreads.vue @@ -0,0 +1,141 @@ + + + + diff --git a/frontend/src/components/threaddump/Diagnose.vue b/frontend/src/components/threaddump/Diagnose.vue new file mode 100644 index 00000000..a48c324f --- /dev/null +++ b/frontend/src/components/threaddump/Diagnose.vue @@ -0,0 +1,145 @@ + + + + + + diff --git a/frontend/src/components/threaddump/Thread.vue b/frontend/src/components/threaddump/Thread.vue index 96e903d3..78b06e79 100644 --- a/frontend/src/components/threaddump/Thread.vue +++ b/frontend/src/components/threaddump/Thread.vue @@ -23,6 +23,14 @@ const props = defineProps({ type: { type: String, required: false + }, + threadState: { + type: String, + required: false + }, + ids: { + type: Array as () => number[], + required: false } }); @@ -61,6 +69,7 @@ function loadThreads() { loading.value = true; let groupName = props.groupName; let type = props.type; + let ids = props.ids; let paging = { page: page.value, pageSize @@ -75,6 +84,8 @@ function loadThreads() { : { type, name: name.value, + threadState: props.threadState, + ids: ids && ids.length > 0 ? ids : undefined, ...paging } ).then((pageView) => { diff --git a/frontend/src/components/threaddump/ThreadDump.vue b/frontend/src/components/threaddump/ThreadDump.vue index 7ec002ba..e4497cca 100644 --- a/frontend/src/components/threaddump/ThreadDump.vue +++ b/frontend/src/components/threaddump/ThreadDump.vue @@ -28,15 +28,25 @@ import Content from '@/components/threaddump/Content.vue'; import Thread from '@/components/threaddump/Thread.vue'; import Monitor from '@/components/threaddump/Monitor.vue'; import CallSiteTree from '@/components/threaddump/CallSiteTree.vue'; +import Diagnose from '@/components/threaddump/Diagnose.vue'; +import CpuConsumingThreads from '@/components/threaddump/CpuConsumingThreads.vue'; +import BlockedThreads from '@/components/threaddump/BlockedThreads.vue'; +import ThreadDumpSearch from '@/components/threaddump/ThreadDumpSearch.vue'; +import ThreadDumpOverview from '@/components/threaddump/ThreadDumpOverview.vue'; const { request } = useAnalysisApiRequester(); const activeNames = ref([ 'basicInfo', + 'diagnosis', 'threadSummary', 'threadGroupSummary', + 'blockedThreads', + 'cpuConsumingThreads', 'javaMonitors', - 'callSiteTree' + 'callSiteTree', + 'threadSearch', + 'dumpOverview' ]); const deadLockCount = ref(0); @@ -65,6 +75,7 @@ const loading = ref(false); const threadDialogVisible = ref(false); const selectedThreadType = ref(); const selectedThreadGroup = ref(); +const selectedThreadState = ref(); function sum(arr) { return arr.reduce((l, r) => l + r); @@ -83,15 +94,57 @@ function sortIndices(counts) { function showThreads(type) { selectedThreadType.value = type; selectedThreadGroup.value = null; + selectedThreadState.value = null; threadDialogVisible.value = true; } function showThreadsOfGroup(group) { selectedThreadGroup.value = group; selectedThreadType.value = null; + selectedThreadState.value = null; threadDialogVisible.value = true; } +function showThreadsByState(threadType, state) { + selectedThreadType.value = threadType; + selectedThreadGroup.value = null; + selectedThreadState.value = state; + threadDialogVisible.value = true; +} + +function showThreadsOfGroupByState(group, state) { + selectedThreadGroup.value = group; + selectedThreadType.value = null; + selectedThreadState.value = state; + threadDialogVisible.value = true; +} + +const STATE_COLORS: Record = { + // JavaThreadState enum names (backend serialization) + RUNNABLE: '#67c23a', // green + SLEEPING: '#e6a23c', // orange + IN_OBJECT_WAIT: '#409eff', // blue + IN_OBJECT_WAIT_TIMED: '#79bbff', // light blue + PARKED: '#a855f7', // purple + PARKED_TIMED: '#7c3aed', // dark purple + BLOCKED_ON_MONITOR_ENTER:'#f56c6c', // red + NEW: '#95d475', // light green + TERMINATED: '#909399', // gray + // OSTreadState enum names + MONITOR_WAIT: '#f56c6c', // red + COND_VAR_WAIT: '#5c8ee6', // steel blue + OBJECT_WAIT: '#3b82f6', // blue + BREAK_POINTED: '#f0c419', // yellow + ALLOCATED: '#95d475', // light green + INITIALIZED: '#67c23a', // green + ZOMBIE: '#606266', // dark gray + UNKNOWN: '#606266', // dark gray +}; + +function stateColor(state: string) { + return STATE_COLORS[state] ?? '#8892a4'; +} + onMounted(() => { loading.value = true; request('overview').then((overview) => { @@ -120,16 +173,16 @@ onMounted(() => { } ]; - function buildThreadStat(key, states, counts, icon, threadType?) { - return { - key, - value: sum(counts), - states, - counts, - icon: shallowRef(icon), - threadType - }; - } +function buildThreadStat(key, states, counts, icon, threadType?) { + return { + key, + value: sum(counts), + states, + counts, + icon: shallowRef(icon), + threadType + }; +} let _threadStats = [ buildThreadStat( @@ -182,8 +235,12 @@ onMounted(() => {