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..e3f4cc93 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,15 @@ 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; /** * Thread dump analyzer @@ -176,13 +186,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 +206,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 +336,277 @@ 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. + * Threads without CPU information (i.e. the dump was taken without + * {@code -e} / cpu data) are excluded. + * + * @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) { + return snapshot.getThreadMap().values().stream() + .filter(t -> type == null || t.getType() == type) + .filter(t -> t.getCpu() > 0) + .sorted(Comparator.comparingDouble(Thread::getCpu).reversed()) + .limit(max < 0 ? Integer.MAX_VALUE : max) + .map(this::convertToVThread) + .collect(Collectors.toList()); + } + + /** + * 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 + * @throws IOException if the dump file cannot be read + */ + 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) throws IOException { + 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 = new ArrayList<>(); + for (String t : term) { + try { + patterns.add(Pattern.compile(doRegex ? t : Pattern.quote(t), flags)); + } catch (java.util.regex.PatternSyntaxException e) { + throw new IllegalArgumentException("Invalid regex term: " + t, e); + } + } + + List candidates = 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; + } + candidates.add(t); + }, snapshot.getJavaThreads(), snapshot.getNonJavaThreads()); + + // When the stack trace is not part of the search, match on name/state + // first so that raw content is only read for actual hits. + if (!doSearchStack) { + candidates.removeIf(t -> !matchesAllTerms(patterns, t, null, doSearchName, doSearchState, false)); + } + + Map> contents = readThreadContents(candidates); + + List results = new ArrayList<>(); + for (Thread t : candidates) { + List rawLines = contents.getOrDefault(t.getId(), Collections.emptyList()); + + if (doSearchStack) { + String stackStr = rawLines.size() > 1 + ? String.join("\n", rawLines.subList(1, rawLines.size())) + : ""; + if (!matchesAllTerms(patterns, t, stackStr, doSearchName, doSearchState, true)) { + continue; + } + } + + 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); + } + + return results; + } + + // ------------------------------------------------------------------ + // private helpers + // ------------------------------------------------------------------ + + /** + * Returns {@code true} if every pattern matches at least one of the enabled + * search fields of the given thread (AND semantics across terms). + */ + private boolean matchesAllTerms(List patterns, Thread t, String stackStr, + boolean searchName, boolean searchState, boolean searchStack) { + String nameStr = t.getName() != null ? t.getName() : ""; + String stateStr = getThreadState(t); + return patterns.stream().allMatch(p -> + (searchName && p.matcher(nameStr).find()) + || (searchState && p.matcher(stateStr).find()) + || (searchStack && stackStr != null && p.matcher(stackStr).find()) + ); + } + + /** + * Reads the raw content lines of the given threads in a single sequential + * pass over the dump file (threads are processed in file order). + * + * @return map from thread id to its raw content lines + */ + private Map> readThreadContents(List threads) throws IOException { + Map> contents = new HashMap<>(); + if (threads.isEmpty()) { + return contents; + } + List ordered = new ArrayList<>(threads); + ordered.sort(Comparator.comparingInt(Thread::getLineStart)); + try (LineNumberReader lnr = new LineNumberReader(new FileReader(snapshot.getPath()))) { + int current = 1; + for (Thread t : ordered) { + while (current < t.getLineStart()) { + lnr.readLine(); + current++; + } + List lines = new ArrayList<>(Math.max(t.getLineEnd() - t.getLineStart() + 1, 0)); + while (current <= t.getLineEnd()) { + lines.add(lnr.readLine()); + current++; + } + contents.put(t.getId(), lines); + } + } + return contents; + } + + /** + * 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..ca89b9bb --- /dev/null +++ b/analysis/thread-dump/src/main/java/org/eclipse/jifa/tda/diagnoser/ThreadDumpDiagnoser.java @@ -0,0 +1,211 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 and CC0-1.0 + * + * AI Disclosure: This file was largely AI-generated with GitHub Copilot. + * The AI-generated portions are made available under CC0-1.0. The human + * contributor has reviewed and verified the code. + * Assisted-by: GitHub Copilot (Claude Sonnet 4.5) + ********************************************************************************/ + +package org.eclipse.jifa.tda.diagnoser; + +import org.eclipse.jifa.tda.diagnoser.Diagnostic.Severity; +import org.eclipse.jifa.tda.enums.JavaThreadState; +import org.eclipse.jifa.tda.model.Frame; +import org.eclipse.jifa.tda.model.JavaThread; +import org.eclipse.jifa.tda.model.Snapshot; +import org.eclipse.jifa.tda.model.Thread; +import org.eclipse.jifa.tda.vo.VThread; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +/** + * Analyses a parsed thread dump {@link Snapshot} against a + * {@link ThreadDumpAnalysisConfig} and returns a list of {@link Diagnostic} + * findings. + *

+ * 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) { + if (config.getHighBlockedThreadsThreshold() <= 0) { + return; + } + 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..be16db80 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,84 @@ package org.eclipse.jifa.tda.util; +import java.text.NumberFormat; +import java.text.ParseException; +import java.text.ParsePosition; +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. + *
+ *

+ * {@code Double.parseDouble} already covers all standard (dot-decimal) formats, + * so the German-locale fallback is used only when that first step fails. + * Note: a Locale.US {@code NumberFormat} is intentionally not used as + * an intermediate step because it treats a comma as a thousands separator and + * would mis-parse {@code "1,5"} as {@code 15.0}. + * + * @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 ignore) { + // fall through to locale-aware parsing + } + try { + ParsePosition pos = new ParsePosition(0); + Number n = NumberFormat.getInstance(Locale.GERMANY).parse(clean, pos); + if (n != null && pos.getIndex() == clean.length()) { + return n.doubleValue(); + } + throw new ParseException("Unparseable number: \"" + clean + "\"", pos.getErrorIndex()); + } catch (ParseException ex) { + throw new IllegalArgumentException("Cannot parse '" + s + "' as a number", ex); + } + } } 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..411a42b4 100644 --- a/frontend/components.d.ts +++ b/frontend/components.d.ts @@ -13,8 +13,10 @@ 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'] + ElCollapseTransition: typeof import('element-plus/es')['ElCollapseTransition'] ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider'] ElDatePicker: typeof import('element-plus/es')['ElDatePicker'] ElDescriptions: typeof import('element-plus/es')['ElDescriptions'] @@ -37,6 +39,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..4c376091 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", @@ -34,6 +35,7 @@ "devDependencies": { "@rushstack/eslint-patch": "^1.3.2", "@tsconfig/node18": "^18.2.0", + "@types/d3": "^7.4.3", "@types/jsdom": "^21.1.1", "@types/node": "^18.17.0", "@vitejs/plugin-vue": "^5.2.1", @@ -1535,6 +1537,290 @@ "integrity": "sha512-yhxwIlFVSVcMym3O31HoMnRXpoenmpIxcj4Yoes2DUpe+xCJnA7ECQP1Vw889V0jTt/2nzvpLQ/UuMYCd3JPIg==", "dev": true }, + "node_modules/@types/d3": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz", + "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/d3-axis": "*", + "@types/d3-brush": "*", + "@types/d3-chord": "*", + "@types/d3-color": "*", + "@types/d3-contour": "*", + "@types/d3-delaunay": "*", + "@types/d3-dispatch": "*", + "@types/d3-drag": "*", + "@types/d3-dsv": "*", + "@types/d3-ease": "*", + "@types/d3-fetch": "*", + "@types/d3-force": "*", + "@types/d3-format": "*", + "@types/d3-geo": "*", + "@types/d3-hierarchy": "*", + "@types/d3-interpolate": "*", + "@types/d3-path": "*", + "@types/d3-polygon": "*", + "@types/d3-quadtree": "*", + "@types/d3-random": "*", + "@types/d3-scale": "*", + "@types/d3-scale-chromatic": "*", + "@types/d3-selection": "*", + "@types/d3-shape": "*", + "@types/d3-time": "*", + "@types/d3-time-format": "*", + "@types/d3-timer": "*", + "@types/d3-transition": "*", + "@types/d3-zoom": "*" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-axis": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz", + "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-brush": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz", + "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-chord": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz", + "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-contour": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz", + "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-dispatch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz", + "integrity": "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-drag": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-dsv": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", + "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-fetch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz", + "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-dsv": "*" + } + }, + "node_modules/@types/d3-force": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz", + "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-format": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz", + "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-geo": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz", + "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-hierarchy": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", + "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-polygon": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz", + "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-quadtree": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz", + "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-random": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.4.tgz", + "integrity": "sha512-UHYId5WTCx4L4YNel7NU00XUXXgvgpgZOvp10PuvsQENjMDXhh2RyFc0KBjO7B45ne4Ha1yVH7ii0vnzKkuzWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-selection": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", + "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-shape": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-time-format": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz", + "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-transition": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", + "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", @@ -1542,6 +1828,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/jsdom": { "version": "21.1.1", "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-21.1.1.tgz", @@ -3129,6 +3422,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 +3960,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 +5508,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 +5590,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 +7192,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 +7349,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 +7390,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..f7fa7712 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", @@ -40,6 +41,7 @@ "devDependencies": { "@rushstack/eslint-patch": "^1.3.2", "@tsconfig/node18": "^18.2.0", + "@types/d3": "^7.4.3", "@types/jsdom": "^21.1.1", "@types/node": "^18.17.0", "@vitejs/plugin-vue": "^5.2.1", diff --git a/frontend/src/components/threaddump/BlockedThreads.vue b/frontend/src/components/threaddump/BlockedThreads.vue new file mode 100644 index 00000000..f69b1ff5 --- /dev/null +++ b/frontend/src/components/threaddump/BlockedThreads.vue @@ -0,0 +1,261 @@ + + + + + + diff --git a/frontend/src/components/threaddump/CpuConsumingThreads.vue b/frontend/src/components/threaddump/CpuConsumingThreads.vue new file mode 100644 index 00000000..ab91a066 --- /dev/null +++ b/frontend/src/components/threaddump/CpuConsumingThreads.vue @@ -0,0 +1,221 @@ + + + + diff --git a/frontend/src/components/threaddump/Diagnose.vue b/frontend/src/components/threaddump/Diagnose.vue new file mode 100644 index 00000000..aea78f27 --- /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..1b38be3a 100644 --- a/frontend/src/components/threaddump/ThreadDump.vue +++ b/frontend/src/components/threaddump/ThreadDump.vue @@ -28,15 +28,24 @@ 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 { stateTagStyle } from '@/components/threaddump/thread-state-colors'; const { request } = useAnalysisApiRequester(); const activeNames = ref([ 'basicInfo', + 'diagnosis', 'threadSummary', 'threadGroupSummary', + 'blockedThreads', + 'cpuConsumingThreads', 'javaMonitors', - 'callSiteTree' + 'callSiteTree', + 'threadSearch', ]); const deadLockCount = ref(0); @@ -65,6 +74,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,12 +93,28 @@ 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; } @@ -120,16 +146,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( @@ -170,20 +196,36 @@ onMounted(() => { _threadStats.sort((i, j) => j.value - i.value); _threadGroupStats.sort((i, j) => j.value - i.value); + // The total row merges the per-type distributions so that each state tag + // is consistent with the state used by the backend when filtering threads + // (Java state for Java threads, OS state for the others). + let mergedCounts = new Map(); + for (let stat of _threadStats) { + stat.states.forEach((state, i) => { + if (stat.counts[i] > 0) { + mergedCounts.set(state, (mergedCounts.get(state) || 0) + stat.counts[i]); + } + }); + } _threadStats.push( - buildThreadStat('total', overview.states, overview.threadStat.counts, Histogram) + buildThreadStat('total', [...mergedCounts.keys()], [...mergedCounts.values()], Histogram) ); threadStats.value = _threadStats; threadGroupStats.value = _threadGroupStats; + loading.value = false; }); });