Skip to content

feat(thread-dump): Thread Dump Compare – follow-up to #389 - #394

Open
zachelnet wants to merge 34 commits into
eclipse-jifa:mainfrom
zachelnet:feature/thread-dump-compare
Open

feat(thread-dump): Thread Dump Compare – follow-up to #389#394
zachelnet wants to merge 34 commits into
eclipse-jifa:mainfrom
zachelnet:feature/thread-dump-compare

Conversation

@zachelnet

Copy link
Copy Markdown
Contributor

Overview

This PR adds a Thread Dump Compare page as the follow-up to #389, as agreed with @D-D-H (see comment in #389: "Let's handle it in a separate PR").

It follows the same snapshot-diff methodology already established in Jifa for heap dump histogram comparison and GC log comparison: threads are matched across dumps by their native thread ID (NID), and changes in state, CPU time, and blocking relationships are surfaced.

Closes part of #231 (comparing a series of thread dumps).


Changes

Backend (analysis/thread-dump)

New API endpoints in ThreadDumpAnalyzer:

Method Description
cpuConsumingThreadsCompare(other, type, max) CPU delta between two dumps (matched by NID), sorted descending
threadStateChanges(other) State-changed, new, and disappeared threads between two dumps
persistentBlockers(other) Threads in BLOCKED_ON_MONITOR_ENTER in both dumps
compareMultiple(other1, other2?, other3?) Matrix view for up to 4 dumps (API only; UI follow-up)

New VOs: VThreadDelta, VThreadStateChange, VMultiDumpComparison

Infrastructure fix in AbstractApiExecutor + AnalysisApiArgumentResolverFactory:

  • comparisonTargetPath parameters can now be required = false (needed for optional other2/other3 in compareMultiple)
  • Fixed logic inversion bug in ComparisonPathResolver: !element.isJsonPrimitive() || !element.getAsJsonPrimitive().isString()

Frontend (Vue 3 / TypeScript)

New route /thread-dump-compare with ThreadDumpCompare.vue as the main page, containing:

Section Component
Basic Info (timestamps, VM, JNI) inline in ThreadDumpCompare.vue
Thread Summary + Thread Groups inline
State Distribution StateDistributionCompare.vue
CPU Delta CpuConsumingThreadsCompare.vue
State Changes ThreadStateChanges.vue
Persistent Blockers PersistentBlockers.vue
Diagnosis DiagnoseCompare.vue

UI conventions:

  • Label column left-aligned, value columns right-aligned, Δ column centered
  • Element Plus CSS variables throughout (no hardcoded colors/font-sizes)
  • File picker loads all pages (pagination fix for > 50 files)

Tests

New unit tests in TestAnalyzer:

  • testThreadStateChanges_sameDump – comparing a dump to itself yields no changes
  • testThreadStateChanges_differentDumps – different JVMs produce disappeared + new entries
  • testPersistentBlockers_sameDump – all 4 blocked threads are persistent
  • testPersistentBlockers_noOverlap – no blocked threads → empty result

Note on compareMultiple

The compareMultiple API (up to 4 dumps, matrix view) is implemented in the backend and ready for use. The matrix UI is intentionally left out of this PR to keep the scope focused. It can be added in a follow-up.


AI Disclosure

This contribution was developed with AI assistance.
Assisted-by: GitHub Copilot (Claude Sonnet 4.6)

zachelnet and others added 30 commits July 28, 2026 14:41
…pring Boot 3

Backend (analysis/thread-dump):
- New diagnoser: ThreadDumpDiagnoser, Diagnostic, ThreadDumpAnalysisConfig
- ThreadDumpAnalyzer: diagnose(), blockingThreads(), cpuConsumingThreads(),
  cpuConsumingThreadsCompare(), searchThreads(), threads() with state/id filter
- Converter: robust locale-aware number parsing
- New VOs: SearchHit, VBlockingThread, extended VThread
- Tests: TestDiagnoser, TestConverter, TestAnalyzer updated

Frontend (Vue 3):
- New components: Diagnose, BlockedThreads (D3 tree), CpuConsumingThreads
  (ECharts bar), ThreadDumpSearch, ThreadDumpSearchForm, ThreadDumpOverview
- ThreadDump.vue: thread summary with clickable colored el-tags per state
- ThreadDumpOverview.vue: ECharts Doughnut + Bar charts with dark mode,
  resize handler and onUnmounted cleanup
- STATE_COLORS covers all JavaThreadState + OSTreadState enum values
- i18n: en.ts + zh.ts for all new keys

Assisted-by: GitHub Copilot (Claude Sonnet 4.5)
…iagnosis (Basic Information)

Assisted-by: GitHub Copilot (Claude Sonnet 4.6)
Assisted-by: GitHub Copilot (Claude Sonnet 4.6)
Backend (ThreadDumpAnalyzer):
- remove cpuConsumingThreadsCompare API whose parameter cannot be
  marshalled through the analysis API layer and which has no consumer
- cpuConsumingThreads: exclude threads without cpu data (cpu <= 0)
- searchThreads: apply name/state filters before any file I/O and read
  matched thread contents in a single sequential pass; propagate
  IOException instead of swallowing it

Frontend:
- ThreadDump: drop the state distribution pie chart; build the Total
  row state tags by merging per-type distributions (Java states for
  Java threads, OS states otherwise) so counts and tag clicks are
  consistent with backend thread filtering
- thread-state-colors: soft tinted tag style (light background,
  colored text/border) with a semantic, less saturated palette;
  works in light and dark themes
- ThreadDumpSearch: render results in a table (expandable stack rows,
  sortable CPU/elapsed columns) with an inline summary bar; stack
  content style matches Thread.vue
- ThreadDumpSearchForm: remove card wrapper, inline search input with
  appended button and 'More Options' toggle, left-aligned layout,
  aligned labels and section titles
- CpuConsumingThreads: query JIT/GC/VM types separately for the
  non-Java chart; center x-axis name to avoid clipped label
- Diagnose: shared table header style and icon/message alignment
- i18n: rename search section to 'Find Threads', clearer placeholder,
  rename keys (matchFields/matchFieldXxx/otherOptions)

Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
Add VThreadDelta VO and cpuConsumingThreadsCompare() method to
ThreadDumpAnalyzer. Matches threads between two dumps by native ID,
computes the CPU-time delta and returns the top-N results sorted
descending by delta.

Uses @ApiParameterMeta(comparisonTargetPath = true) to resolve the
second dump's uniqueName to a Path via the existing
ComparisonPathResolver infrastructure.

Assisted-by: GitHub Copilot (Claude Sonnet 4.6)
Add a dedicated /thread-dump-compare route accessible via the sidebar.
The page lets the user pick two THREAD_DUMP files and shows:

- Basic Information side-by-side (timestamp delta, VM info match/mismatch)
- Thread Summary table with Dump1 | Δ | Dump2 columns (▲/▼ highlighted)
- Thread Group Summary table with delta highlighting and new-group markers
- CPU Time Delta chart (reuses CpuConsumingThreadsCompare component)

CpuConsumingThreadsCompare.vue refactored from standalone dropdown
component to a pure chart receiving file1/file2 as props, using
requestWithTarget() instead of direct axios.

Remove the temporary cpuConsumingThreadsCompare collapse-item from
the single-file ThreadDump view.

Assisted-by: GitHub Copilot (Claude Sonnet 4.6)
…omponent

Vite build failed because el-collapse-item-like is not a real Element
Plus component. Merge all sections into a single el-collapse with four
named el-collapse-item children.

Assisted-by: GitHub Copilot (Claude Sonnet 4.6)
The condition used || instead of &&, causing any string value
(including valid uniqueNames) to be rejected with
'must be the name of a comparison target'.

Assisted-by: GitHub Copilot (Claude Sonnet 4.6)
Add /thread-dump-compare to the Spring MVC view controller list so
direct URL access and browser refresh forward to index.html instead
of returning 404 NoResourceFoundException.

Assisted-by: GitHub Copilot (Claude Sonnet 4.6)
All three tables (Basic Info, Thread Summary, Thread Groups) now use
the same 4-column layout with identical widths:
- Col 1 label/type: min-width 200px
- Col 2 Dump1:      min-width 220px
- Col 3 Delta:      fixed 130px, centered
- Col 4 Dump2:      min-width 220px

Remove separate col-header div, use table headers consistently.
Remove extra icon column from Basic Info, integrate icon into label cell.

Assisted-by: GitHub Copilot (Claude Sonnet 4.6)
The values in the 'Basic Information' table were still left-aligned.
This commit applies 'align="right"' to the value columns in that
table as well, making the alignment consistent across all three
comparison tables.

Assisted-by: GitHub Copilot (Claude Sonnet 4.6)
- Show upload prompt with icon, title and hint text when no thread
  dump files are available, with a direct link to the files page
- Show a simple hint when files exist but none are selected yet
- Add i18n keys: noFilesAvailable, noFilesAvailableHint, uploadNow (en + zh)

Assisted-by: GitHub Copilot (Claude Sonnet 4.6)
…hanges and persistent blockers sections

- Add Diagnosis section to compare page showing both dumps side-by-side
  Extend Diagnose.vue with optional target prop for use outside analysis store context
- Add StateDistributionCompare component: Java thread state table with Dump1/Δ/Dump2
  columns, consistent with Thread Summary and Thread Group Summary UX pattern
- Add ThreadStateChanges component: summary table (changed/disappeared/new) plus
  state transition breakdown grouped by from→to pairs
- Add PersistentBlockers component: summary table with resolved/introduced metrics
  plus list of threads blocked in both dumps with inspect dialog
- Add VThreadStateChange VO and two new backend API methods:
  threadStateChanges(Path other) and persistentBlockers(Path other)
- Align all new table column widths (min-width 200/220) to match existing sections
- Add i18n keys for all new sections in en.ts

Assisted-by: GitHub Copilot (Claude Sonnet 4.6)
- Remove duplicate interface ThreadDelta block rendered as raw text after
  </template> in CpuConsumingThreadsCompare.vue (B1)
- Fix paging parameters in PersistentBlockers: send flat {page, pageSize}
  instead of nested {paging: {page, pageSize}} to match server API (B2)
- Pass actual file names as column headers to ThreadStateChanges and
  PersistentBlockers (were showing generic 'Dump 1'/'Dump 2') (D1)
- Remove 'State changed' summary row: count1=count2=N with delta=0 was
  confusing; transition breakdown table conveys the same info (D2)
- Gate 'State transition breakdown' h4 header behind v-if to avoid empty
  heading when no threads changed state (D3)
- Gate 'Threads blocked in both dumps' h4 header behind v-if when no
  persistent blockers exist (D4)
- Remove dead CSS classes from ThreadDumpCompare.vue (T4)

Assisted-by: GitHub Copilot (Claude Sonnet 4.6)
…ntax and tests

- Fix Examine button in DiagnoseCompare: add optional target prop to
  Thread.vue so it uses requestWithTarget when called from compare page,
  avoiding TypeError on missing analysis store (B3)
- Fix State Distribution label column width: 260 -> 200 to match all
  other compare table sections (D6)
- Fix zh.ts syntax error: missing closing } for threadDumpCompare block
  causing build failure
- Add --add-opens=java.base/java.lang=ALL-UNNAMED to thread-dump test JVM
  args to fix cglib incompatibility with Java 17 in new tests (T5)

Assisted-by: GitHub Copilot (Claude Sonnet 4.6)
…components

- Move deltaClass(), deltaText() and PageView<T> into thread-state-colors.ts
  as shared exports; remove 4 identical local copies across compare components
- Replace local stateType() in ThreadStateChanges with stateTagStyle() from
  thread-state-colors.ts for full state color coverage (17 states vs 4)
- Remove duplicate :deep(.delta-*) CSS from child components; delta classes
  are defined once in ThreadDumpCompare parent via :deep and cascade down

Assisted-by: GitHub Copilot (Claude Sonnet 4.6)
…ntBlockers

- testThreadStateChanges_sameDump: same dump yields no changes
- testThreadStateChanges_differentDumps: different JVMs yield disappeared/new
- testPersistentBlockers_sameDump: all 4 blocked threads are persistent
- testPersistentBlockers_noOverlap: no overlap across different JVM dumps
- testCpuConsumingThreadsCompare_sameDump: delta is 0 for all threads

Assisted-by: GitHub Copilot (Claude Sonnet 4.6)
- Reorder collapse sections: State Distribution, Thread State Changes and
  Persistent Blockers before CPU Delta (CPU is often empty, should not
  block visible content above the fold) (Dx3)
- Gate el-divider in ThreadStateChanges: only show when there are state
  changes or non-empty results to separate (Dx1)
- Add error notification to loadFiles() when API call fails (Ex2)
- Add 'Compare with another dump' button to single-dump view (ThreadDump.vue)
  that navigates to compare page with file1 pre-selected via query param (Po1)

Assisted-by: GitHub Copilot (Claude Sonnet 4.6)
… | Δ | Dump2)

Diagnosis table had 3 columns while all other compare tables use 4.
- Reduce Dump1/Dump2 min-width from 300 to 220 to fit Δ column
- Add Δ column (width 130): New tag / Fixed tag / = for consistent visual

Assisted-by: GitHub Copilot (Claude Sonnet 4.6)
New endpoint compareMultiple(other1, other2?, other3?) returns a unified
VMultiDumpComparison with:
- DumpSummary per dump: name, threadCount, deadLockCount, stateCounts map
- ThreadRow per unique NID: name, states[n] (null = not present), alwaysBlocked, stateChanged

Rows sorted: persistently blocked first, then state-changed, then rest.
Supports 2–4 dumps in a single API call.

Usage:
  POST /jifa-api/analysis
  { namespace: 'thread-dump', api: 'compareMultiple',
    target: 'uuid1',
    parameters: { other1: 'uuid2', other2: 'uuid3' } }

Assisted-by: GitHub Copilot (Claude Sonnet 4.6)
The resolver was hardcoded with required=true and ignored the
@ApiParameterMeta(required = false) annotation. This caused optional
comparison paths (other2, other3 in compareMultiple) to always fail
with 'is required'.

Pass the required flag through from buildLeafResolver and return null
when the key is absent and required=false.

Assisted-by: GitHub Copilot (Claude Sonnet 4.6)
AbstractApiExecutor.buildApiParameters() forced required=true for all
Path parameters with comparisonTargetPath=true, overriding any
@ApiParameterMeta(required=false) annotation. Only targetPath parameters
should be unconditionally required; comparisonTargetPath parameters
must respect the required flag from the annotation.

This fixes compareMultiple(other1, other2?, other3?) where other2 and
other3 are optional.

Assisted-by: GitHub Copilot (Claude Sonnet 4.6)
- Fix CpuConsumingThreadsCompare Thread dialog: pass target prop so
  click-to-inspect works on compare page (Bug eclipse-jifa#6)
- Add JavaThreadState and LinkedHashMap imports to ThreadDumpAnalyzer;
  remove fully-qualified usages (Code Quality eclipse-jifa#2, eclipse-jifa#4)
- Fix threadStateChanges sort order comment: 'changed, new, disappeared'
  not 'changed, disappeared, new' (Comment eclipse-jifa#3)
- Remove unused isStateChanged() from VThreadStateChange (Dead Code eclipse-jifa#5)

Assisted-by: GitHub Copilot (Claude Sonnet 4.6)
…compare tables

Assisted-by: GitHub Copilot (Claude Sonnet 4.6)
…arations

Assisted-by: GitHub Copilot (Claude Sonnet 4.6)
…eaders, empty states)

Assisted-by: GitHub Copilot (Claude Sonnet 4.6)
…ct, rename CPU label

Assisted-by: GitHub Copilot (Claude Sonnet 4.6)
- Correct threadGroupSummary from thread pool to thread group
- Use precise Java monitor and call site tree terminology
- Replace CPU delta/increment wording with neutral change wording
- Align noCpuData terminology with thread dump naming

Assisted-by: GitHub Copilot (Kimi K2.7 Code)
Assisted-by: GitHub Copilot (Claude Sonnet 4.6)
…t DecimalFormatSymbols

Assisted-by: GitHub Copilot (Claude Sonnet 4.6)
…e file picker

- threadStateChanges: use second.getId() instead of -1 for threads only in the
  second dump, so the frontend can open the thread detail view
- ThreadDumpCompare: fetch all pages instead of hardcoded pageSize=200

Assisted-by: GitHub Copilot (Claude Sonnet 4.6)
@zachelnet

Copy link
Copy Markdown
Contributor Author

Compare in thread-dump-analysis:
Bildschirmfoto_29-7-2026_144323_localhost

Selected second thread-dump:
Bildschirmfoto_29-7-2026_144343_localhost

Bildschirmfoto_29-7-2026_144433_localhost Bildschirmfoto_29-7-2026_144354_localhost

…format

DecimalFormat with only decimalSeparator=',' could not parse '1.234,56'
because the dot was not recognised as a grouping separator.

Fix: set groupingSeparator='.' and use pattern '#,##0.##' with ParsePosition
to ensure the full string is consumed.

Assisted-by: GitHub Copilot (Claude Sonnet 4.6)
- Pagination: use Infinity fallback instead of 0 when totalSize missing,
  preventing early termination after the first page
- Empty state: show loading spinner while filesLoading, not el-empty
- onMounted: capture preselect query param before async loadFiles()
- groupRows: use optional chaining for threadGroupStat[k].counts

Assisted-by: GitHub Copilot (Claude Sonnet 4.6)
Also auto-triggers comparison when both file1 and file2 are preset via URL.

Assisted-by: GitHub Copilot (Claude Sonnet 4.6)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants