Skip to content

Commit 20be784

Browse files
committed
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
1 parent 9dc1937 commit 20be784

26 files changed

Lines changed: 3109 additions & 45 deletions

NOTICE

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,3 +175,4 @@ permitted.
175175

176176
Alibaba - Initial implementation
177177
Netflix - Basic customization hooks
178+
SEEBURGER - ThreadDump enhancements

analysis/thread-dump/src/main/java/org/eclipse/jifa/tda/ThreadDumpAnalyzer.java

Lines changed: 288 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@
2121
import org.eclipse.jifa.common.domain.request.PagingRequest;
2222
import org.eclipse.jifa.common.domain.vo.PageView;
2323
import org.eclipse.jifa.common.util.PageViewBuilder;
24+
import org.eclipse.jifa.tda.diagnoser.Diagnostic;
25+
import org.eclipse.jifa.tda.diagnoser.ThreadDumpAnalysisConfig;
26+
import org.eclipse.jifa.tda.diagnoser.ThreadDumpDiagnoser;
2427
import org.eclipse.jifa.tda.enums.MonitorState;
2528
import org.eclipse.jifa.tda.enums.ThreadType;
2629
import org.eclipse.jifa.tda.model.CallSiteTree;
@@ -35,6 +38,8 @@
3538
import org.eclipse.jifa.tda.util.CollectionUtil;
3639
import org.eclipse.jifa.tda.vo.Content;
3740
import org.eclipse.jifa.tda.vo.Overview;
41+
import org.eclipse.jifa.tda.vo.SearchHit;
42+
import org.eclipse.jifa.tda.vo.VBlockingThread;
3843
import org.eclipse.jifa.tda.vo.VFrame;
3944
import org.eclipse.jifa.tda.vo.VMonitor;
4045
import org.eclipse.jifa.tda.vo.VThread;
@@ -44,10 +49,16 @@
4449
import java.io.LineNumberReader;
4550
import java.nio.file.Path;
4651
import java.util.ArrayList;
52+
import java.util.Arrays;
4753
import java.util.Collections;
54+
import java.util.Comparator;
4855
import java.util.HashMap;
4956
import java.util.List;
5057
import java.util.Map;
58+
import java.util.Map.Entry;
59+
import java.util.regex.Pattern;
60+
import java.util.stream.Collectors;
61+
import java.util.stream.Stream;
5162

5263
/**
5364
* Thread dump analyzer
@@ -176,13 +187,17 @@ private PageView<VThread> buildVThreadPageView(List<Thread> threads, PagingReque
176187
}
177188

178189
/**
179-
* @param name the thread name
180-
* @param type the thread type
181-
* @param paging paging request
182-
* @return the threads filtered by name and type
190+
* @param name the thread name filter (substring match, optional)
191+
* @param type the thread type filter (optional)
192+
* @param threadState the Java or OS thread state to filter by (optional)
193+
* @param ids explicit list of thread ids to include (optional)
194+
* @param paging paging request
195+
* @return the threads filtered by name, type, state and/or id
183196
*/
184197
public PageView<VThread> threads(@ApiParameterMeta(required = false) String name,
185198
@ApiParameterMeta(required = false) ThreadType type,
199+
@ApiParameterMeta(required = false) String threadState,
200+
@ApiParameterMeta(required = false) List<Integer> ids,
186201
PagingRequest paging) {
187202
List<Thread> threads = new ArrayList<>();
188203
CollectionUtil.forEach(t -> {
@@ -192,12 +207,29 @@ public PageView<VThread> threads(@ApiParameterMeta(required = false) String name
192207
if (StringUtils.isNotBlank(name) && !t.getName().contains(name)) {
193208
return;
194209
}
210+
if (StringUtils.isNotBlank(threadState) && !getThreadState(t).equals(threadState)) {
211+
return;
212+
}
213+
if (ids != null && !ids.isEmpty() && !ids.contains(t.getId())) {
214+
return;
215+
}
195216
threads.add(t);
196217
}, snapshot.getJavaThreads(), snapshot.getNonJavaThreads());
197218

198219
return buildVThreadPageView(threads, paging);
199220
}
200221

222+
/** Returns the most specific state string for a thread (Java state if available, else OS state). */
223+
private String getThreadState(Thread t) {
224+
if (t instanceof JavaThread) {
225+
JavaThread jt = (JavaThread) t;
226+
if (jt.getJavaThreadState() != null) {
227+
return String.valueOf(jt.getJavaThreadState());
228+
}
229+
}
230+
return String.valueOf(t.getOsThreadState());
231+
}
232+
201233
/**
202234
* @param groupName the thread group name
203235
* @param paging paging request
@@ -305,4 +337,256 @@ public Map<MonitorState, Integer> threadCountsByMonitor(int id) {
305337
map.forEach((s, l) -> counts.put(s, l.size()));
306338
return counts;
307339
}
340+
341+
/**
342+
* Returns all threads that are blocking at least one other thread via monitor
343+
* ownership, sorted descending by number of blocked threads, then by blocker
344+
* thread name.
345+
*
346+
* @return list of blocking threads with their blocked threads and held monitor
347+
*/
348+
public List<VBlockingThread> blockingThreads() {
349+
List<VBlockingThread> result = new ArrayList<>();
350+
351+
Map<Integer, Map<MonitorState, List<Thread>>> allMonitors = snapshot.getMonitorThreads();
352+
for (Entry<Integer, Map<MonitorState, List<Thread>>> monitorEntry : allMonitors.entrySet()) {
353+
Map<MonitorState, List<Thread>> monitorMap = monitorEntry.getValue();
354+
if (!monitorMap.containsKey(MonitorState.LOCKED)) {
355+
continue;
356+
}
357+
Thread blockingThread = monitorMap.get(MonitorState.LOCKED).stream().findFirst().orElse(null);
358+
List<Thread> blockedThreads = new ArrayList<>();
359+
if (monitorMap.containsKey(MonitorState.WAITING_TO_LOCK)) {
360+
blockedThreads.addAll(monitorMap.get(MonitorState.WAITING_TO_LOCK));
361+
}
362+
if (monitorMap.containsKey(MonitorState.WAITING_TO_RE_LOCK)) {
363+
blockedThreads.addAll(monitorMap.get(MonitorState.WAITING_TO_RE_LOCK));
364+
}
365+
if (!blockedThreads.isEmpty() && blockingThread != null) {
366+
VBlockingThread r = new VBlockingThread();
367+
r.setBlockedThreads(blockedThreads.stream()
368+
.map(this::convertToVThread)
369+
.collect(Collectors.toList()));
370+
r.setBlockingThread(convertToVThread(blockingThread));
371+
Monitor mon = findBlockingMonitor(blockedThreads.get(0));
372+
if (mon != null) {
373+
r.setHeldLock(new VMonitor(
374+
mon.getRawMonitor().getId(),
375+
mon.getRawMonitor().getAddress(),
376+
mon.getRawMonitor().isClassInstance(),
377+
mon.getRawMonitor().getClazz(),
378+
mon.getState()));
379+
}
380+
result.add(r);
381+
}
382+
}
383+
384+
result.sort(Comparator
385+
.<VBlockingThread>comparingInt(m -> m.getBlockedThreads().size())
386+
.reversed()
387+
.thenComparing(m -> m.getBlockingThread().getName()));
388+
return result;
389+
}
390+
391+
/**
392+
* Returns the threads with the highest CPU usage, in descending order.
393+
*
394+
* @param type limit to threads of this type; {@code null} means all types
395+
* @param max maximum number of results; {@code -1} means unlimited
396+
* @return list of threads sorted from most to least CPU-intensive
397+
*/
398+
public List<VThread> cpuConsumingThreads(@ApiParameterMeta(required = false) ThreadType type,
399+
int max) {
400+
Stream<Thread> stream = snapshot.getThreadMap().values().stream()
401+
.filter(t -> type == null || t.getType() == type);
402+
stream = stream.sorted(Comparator.comparingDouble(Thread::getCpu).reversed())
403+
.limit(max < 0 ? Integer.MAX_VALUE : max);
404+
return stream.map(this::convertToVThread).collect(Collectors.toList());
405+
}
406+
407+
/**
408+
* Computes which threads consumed the most CPU <em>between</em> two thread
409+
* dumps by matching threads via their native thread id ({@code tid}).
410+
*
411+
* @param other the second (later) thread dump analyzer
412+
* @param max maximum number of results; {@code -1} means unlimited
413+
* @param type limit to threads of this type; {@code null} means all types
414+
* @return threads sorted by delta-CPU descending
415+
*/
416+
public List<VThread> cpuConsumingThreadsCompare(ThreadDumpAnalyzer other, int max,
417+
@ApiParameterMeta(required = false) ThreadType type) {
418+
int limit = max < 0 ? Integer.MAX_VALUE : max;
419+
Map<Thread, Double> cpuDelta = new HashMap<>();
420+
for (Thread first : snapshot.getThreadMap().values()) {
421+
if (type != null && first.getType() != type) {
422+
continue;
423+
}
424+
Thread second = other.snapshot.getThreadMap().values().stream()
425+
.filter(t -> t.getTid() == first.getTid())
426+
.findFirst().orElse(null);
427+
if (second != null && second.getCpu() > 0) {
428+
cpuDelta.put(first, second.getCpu() - first.getCpu());
429+
}
430+
}
431+
List<VThread> result = new ArrayList<>();
432+
cpuDelta.entrySet().stream()
433+
.sorted(Collections.reverseOrder(Map.Entry.comparingByValue()))
434+
.limit(limit)
435+
.forEach(e -> {
436+
VThread vt = convertToVThread(e.getKey());
437+
vt.setCpu(e.getValue());
438+
result.add(vt);
439+
});
440+
return result;
441+
}
442+
443+
/**
444+
* Diagnoses the thread dump for potential issues based on the given
445+
* configuration and returns any issues found.
446+
*
447+
* @param config the configuration to use; a default config is used if {@code null}
448+
* @return potentially empty list of diagnostic issues
449+
*/
450+
public List<Diagnostic> diagnose(
451+
@ApiParameterMeta(required = false) ThreadDumpAnalysisConfig config) {
452+
return new ThreadDumpDiagnoser().analyze(
453+
snapshot,
454+
config != null ? config : new ThreadDumpAnalysisConfig());
455+
}
456+
457+
/**
458+
* Searches through all threads and returns those whose name, state or
459+
* stack trace match all of the given terms.
460+
*
461+
* @param term search terms; each term must match at least one of the
462+
* enabled search fields (AND semantics across terms)
463+
* @param searchName include the thread name in the search (default: true)
464+
* @param searchState include the thread state in the search (default: true)
465+
* @param searchStack include the stack trace in the search (default: true)
466+
* @param regex treat terms as regular expressions (default: false)
467+
* @param matchCase perform a case-sensitive search (default: false)
468+
* @param allowedJavaStates if non-empty, only include threads whose Java state is
469+
* one of these values
470+
* @return matching threads together with their raw content lines
471+
*/
472+
public List<SearchHit> searchThreads(
473+
@ApiParameterMeta(required = false) List<String> term,
474+
@ApiParameterMeta(required = false) Boolean searchName,
475+
@ApiParameterMeta(required = false) Boolean searchState,
476+
@ApiParameterMeta(required = false) Boolean searchStack,
477+
@ApiParameterMeta(required = false) Boolean regex,
478+
@ApiParameterMeta(required = false) Boolean matchCase,
479+
@ApiParameterMeta(required = false) List<String> allowedJavaStates) {
480+
if (term == null || term.isEmpty()) {
481+
return Collections.emptyList();
482+
}
483+
484+
boolean doSearchName = !Boolean.FALSE.equals(searchName);
485+
boolean doSearchState = !Boolean.FALSE.equals(searchState);
486+
boolean doSearchStack = !Boolean.FALSE.equals(searchStack);
487+
boolean doRegex = Boolean.TRUE.equals(regex);
488+
int flags = Boolean.TRUE.equals(matchCase) ? 0 : Pattern.CASE_INSENSITIVE;
489+
490+
List<Pattern> patterns = term.stream()
491+
.map(t -> Pattern.compile(doRegex ? t : Pattern.quote(t), flags))
492+
.collect(Collectors.toList());
493+
494+
List<SearchHit> results = new ArrayList<>();
495+
496+
CollectionUtil.forEach(t -> {
497+
// Optional state pre-filter
498+
if (allowedJavaStates != null && !allowedJavaStates.isEmpty()) {
499+
if (!(t instanceof JavaThread)) return;
500+
JavaThread jt = (JavaThread) t;
501+
String state = jt.getJavaThreadState() != null
502+
? String.valueOf(jt.getJavaThreadState()) : "";
503+
if (!allowedJavaStates.contains(state)) return;
504+
}
505+
506+
List<String> rawLines;
507+
try {
508+
rawLines = rawContentOfThread(t.getId());
509+
} catch (IOException e) {
510+
rawLines = Collections.emptyList();
511+
}
512+
513+
String nameStr = t.getName() != null ? t.getName() : "";
514+
String stateStr = getThreadState(t);
515+
String stackStr = rawLines.size() > 1
516+
? String.join("\n", rawLines.subList(1, rawLines.size()))
517+
: "";
518+
519+
boolean matches = patterns.stream().allMatch(p ->
520+
(doSearchName && p.matcher(nameStr).find())
521+
|| (doSearchState && p.matcher(stateStr).find())
522+
|| (doSearchStack && p.matcher(stackStr).find())
523+
);
524+
525+
if (matches) {
526+
SearchHit hit = new SearchHit();
527+
hit.setId(t.getId());
528+
hit.setName(t.getName());
529+
hit.setOsState(String.valueOf(t.getOsThreadState()));
530+
if (t instanceof JavaThread) {
531+
JavaThread jt = (JavaThread) t;
532+
if (jt.getJavaThreadState() != null) {
533+
hit.setJavaState(String.valueOf(jt.getJavaThreadState()));
534+
}
535+
}
536+
if (t.getCpu() > 0) hit.setCpu(t.getCpu());
537+
if (t.getElapsed() > 0) hit.setElapsed(t.getElapsed());
538+
hit.setLines(rawLines);
539+
results.add(hit);
540+
}
541+
}, snapshot.getJavaThreads(), snapshot.getNonJavaThreads());
542+
543+
return results;
544+
}
545+
546+
// ------------------------------------------------------------------
547+
// private helpers
548+
// ------------------------------------------------------------------
549+
550+
/**
551+
* Converts a model {@link Thread} to a lightweight {@link VThread} VO,
552+
* copying cpu and elapsed times when available ({@code > 0}).
553+
*/
554+
private VThread convertToVThread(Thread thread) {
555+
VThread vt = new VThread();
556+
vt.setId(thread.getId());
557+
vt.setName(thread.getName());
558+
if (thread.getCpu() > 0) {
559+
vt.setCpu(thread.getCpu());
560+
}
561+
if (thread.getElapsed() > 0) {
562+
vt.setElapsed(thread.getElapsed());
563+
}
564+
return vt;
565+
}
566+
567+
/**
568+
* Finds the monitor that a blocked thread is waiting to acquire by inspecting
569+
* the thread-level monitor list and the first frame that carries monitor
570+
* information.
571+
*/
572+
private Monitor findBlockingMonitor(Thread thread) {
573+
List<Monitor> candidates = new ArrayList<>();
574+
if (thread instanceof JavaThread) {
575+
JavaThread blockedThread = (JavaThread) thread;
576+
if (blockedThread.getTrace() != null && blockedThread.getTrace().getFrames() != null) {
577+
for (Frame frame : blockedThread.getTrace().getFrames()) {
578+
if (frame.getMonitors() != null) {
579+
Arrays.stream(frame.getMonitors()).forEach(candidates::add);
580+
// only the first frame with monitors is relevant
581+
break;
582+
}
583+
}
584+
}
585+
}
586+
return candidates.stream()
587+
.filter(m -> m.getState() == MonitorState.WAITING_TO_LOCK
588+
|| m.getState() == MonitorState.WAITING_TO_RE_LOCK)
589+
.findFirst()
590+
.orElse(null);
591+
}
308592
}

0 commit comments

Comments
 (0)