mcp-jvm-diagnostics 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +165 -0
- package/build/analyzers/contention.js +64 -0
- package/build/analyzers/deadlock.js +89 -0
- package/build/analyzers/gc-pressure.js +111 -0
- package/build/analyzers/heap-diff.js +152 -0
- package/build/index.js +474 -0
- package/build/license.js +114 -0
- package/build/parsers/gc-log.js +100 -0
- package/build/parsers/heap-histo.js +121 -0
- package/build/parsers/jfr-summary.js +160 -0
- package/build/parsers/thread-dump.js +118 -0
- package/package.json +59 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GC log parser.
|
|
3
|
+
*
|
|
4
|
+
* Supports:
|
|
5
|
+
* - Unified JVM logging format (-Xlog:gc*) — Java 9+
|
|
6
|
+
* - G1, ZGC, Parallel, Serial GC
|
|
7
|
+
* - Legacy -verbose:gc format (basic support)
|
|
8
|
+
*/
|
|
9
|
+
// Unified logging: [0.123s][info][gc] GC(0) Pause Young (Normal) (G1 Evacuation Pause) 24M->8M(256M) 1.234ms
|
|
10
|
+
const UNIFIED_GC_RE = /\[(\d+[.,]\d+)s\].*?GC\(\d+\)\s+(Pause\s+\S+(?:\s+\([^)]*\))*)\s+(\d+)M->(\d+)M\((\d+)M\)\s+(\d+[.,]\d+)ms/;
|
|
11
|
+
// Unified concurrent: [0.123s][info][gc] GC(0) Concurrent Mark 1.234ms
|
|
12
|
+
const UNIFIED_CONCURRENT_RE = /\[(\d+[.,]\d+)s\].*?GC\(\d+\)\s+(Concurrent\s+\S+(?:\s+\S+)?)\s+(\d+[.,]\d+)ms/;
|
|
13
|
+
// Legacy format: [GC (Allocation Failure) 65536K->12345K(251392K), 0.0123456 secs]
|
|
14
|
+
const LEGACY_GC_RE = /\[(Full )?GC\s*\(([^)]+)\)\s+(\d+)K->(\d+)K\((\d+)K\),\s+(\d+[.,]\d+)\s+secs\]/;
|
|
15
|
+
// ZGC format: [0.123s][info][gc] GC(0) Garbage Collection (Warmup) 24M(1%)->8M(0%) 1.234ms
|
|
16
|
+
const ZGC_RE = /\[(\d+[.,]\d+)s\].*?GC\(\d+\)\s+Garbage Collection\s+\([^)]+\)\s+(\d+)M\(\d+%\)->(\d+)M\(\d+%\)\s+(\d+[.,]\d+)ms/;
|
|
17
|
+
export function parseGcLog(text) {
|
|
18
|
+
const lines = text.replace(/\r\n/g, "\n").split("\n");
|
|
19
|
+
const events = [];
|
|
20
|
+
let algorithm = "Unknown";
|
|
21
|
+
// Detect algorithm from log content
|
|
22
|
+
if (text.includes("Using G1"))
|
|
23
|
+
algorithm = "G1";
|
|
24
|
+
else if (text.includes("Using ZGC"))
|
|
25
|
+
algorithm = "ZGC";
|
|
26
|
+
else if (text.includes("Using Parallel"))
|
|
27
|
+
algorithm = "Parallel";
|
|
28
|
+
else if (text.includes("Using Serial"))
|
|
29
|
+
algorithm = "Serial";
|
|
30
|
+
else if (text.includes("Using Shenandoah"))
|
|
31
|
+
algorithm = "Shenandoah";
|
|
32
|
+
else if (text.includes("G1 Evacuation") || text.includes("G1 Humongous"))
|
|
33
|
+
algorithm = "G1";
|
|
34
|
+
else if (text.includes("Garbage Collection ("))
|
|
35
|
+
algorithm = "ZGC";
|
|
36
|
+
else if (text.includes("PSYoungGen") || text.includes("ParOldGen"))
|
|
37
|
+
algorithm = "Parallel";
|
|
38
|
+
for (const line of lines) {
|
|
39
|
+
// Try unified format first
|
|
40
|
+
const unifiedMatch = line.match(UNIFIED_GC_RE);
|
|
41
|
+
if (unifiedMatch) {
|
|
42
|
+
events.push({
|
|
43
|
+
timestamp: parseFloat(unifiedMatch[1].replace(",", ".")),
|
|
44
|
+
type: unifiedMatch[2].trim(),
|
|
45
|
+
pauseMs: parseFloat(unifiedMatch[6].replace(",", ".")),
|
|
46
|
+
heapBeforeMb: parseInt(unifiedMatch[3], 10),
|
|
47
|
+
heapAfterMb: parseInt(unifiedMatch[4], 10),
|
|
48
|
+
heapTotalMb: parseInt(unifiedMatch[5], 10),
|
|
49
|
+
});
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
// Try ZGC format
|
|
53
|
+
const zgcMatch = line.match(ZGC_RE);
|
|
54
|
+
if (zgcMatch) {
|
|
55
|
+
events.push({
|
|
56
|
+
timestamp: parseFloat(zgcMatch[1].replace(",", ".")),
|
|
57
|
+
type: "Pause Young (ZGC)",
|
|
58
|
+
pauseMs: parseFloat(zgcMatch[4].replace(",", ".")),
|
|
59
|
+
heapBeforeMb: parseInt(zgcMatch[2], 10),
|
|
60
|
+
heapAfterMb: parseInt(zgcMatch[3], 10),
|
|
61
|
+
heapTotalMb: 0,
|
|
62
|
+
});
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
// Try unified concurrent
|
|
66
|
+
const concurrentMatch = line.match(UNIFIED_CONCURRENT_RE);
|
|
67
|
+
if (concurrentMatch) {
|
|
68
|
+
events.push({
|
|
69
|
+
timestamp: parseFloat(concurrentMatch[1].replace(",", ".")),
|
|
70
|
+
type: concurrentMatch[2].trim(),
|
|
71
|
+
pauseMs: 0, // concurrent phases don't pause
|
|
72
|
+
heapBeforeMb: 0,
|
|
73
|
+
heapAfterMb: 0,
|
|
74
|
+
heapTotalMb: 0,
|
|
75
|
+
});
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
// Try legacy format
|
|
79
|
+
const legacyMatch = line.match(LEGACY_GC_RE);
|
|
80
|
+
if (legacyMatch) {
|
|
81
|
+
const isFull = legacyMatch[1] === "Full ";
|
|
82
|
+
events.push({
|
|
83
|
+
timestamp: 0,
|
|
84
|
+
type: isFull ? "Pause Full" : "Pause Young",
|
|
85
|
+
pauseMs: parseFloat(legacyMatch[6].replace(",", ".")) * 1000,
|
|
86
|
+
heapBeforeMb: Math.round(parseInt(legacyMatch[3], 10) / 1024),
|
|
87
|
+
heapAfterMb: Math.round(parseInt(legacyMatch[4], 10) / 1024),
|
|
88
|
+
heapTotalMb: Math.round(parseInt(legacyMatch[5], 10) / 1024),
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
// Calculate time span
|
|
93
|
+
let timeSpanMs = 0;
|
|
94
|
+
if (events.length > 1) {
|
|
95
|
+
const firstTs = events[0].timestamp;
|
|
96
|
+
const lastTs = events[events.length - 1].timestamp;
|
|
97
|
+
timeSpanMs = (lastTs - firstTs) * 1000;
|
|
98
|
+
}
|
|
99
|
+
return { algorithm, events, timeSpanMs };
|
|
100
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parser for `jmap -histo` output.
|
|
3
|
+
*
|
|
4
|
+
* Example format:
|
|
5
|
+
* num #instances #bytes class name (module)
|
|
6
|
+
* -------------------------------------------------------
|
|
7
|
+
* 1: 123456 12345678 [B (java.base)
|
|
8
|
+
* 2: 98765 9876543 java.lang.String (java.base)
|
|
9
|
+
* 3: 45678 4567800 java.lang.Object[] (java.base)
|
|
10
|
+
* ...
|
|
11
|
+
* Total 500000 50000000
|
|
12
|
+
*/
|
|
13
|
+
// Common JDK internal classes that are expected to be large
|
|
14
|
+
const JDK_INTERNALS = new Set([
|
|
15
|
+
"[B", "[C", "[I", "[J", "[S", "[Z", "[D", "[F",
|
|
16
|
+
"java.lang.String", "java.lang.Object[]", "java.lang.Class",
|
|
17
|
+
"java.util.HashMap$Node", "java.util.concurrent.ConcurrentHashMap$Node",
|
|
18
|
+
"java.lang.reflect.Method", "java.lang.ref.Finalizer",
|
|
19
|
+
]);
|
|
20
|
+
const HISTO_LINE_RE = /^\s*(\d+):\s+(\d+)\s+(\d+)\s+(.+?)(?:\s+\((.+?)\))?\s*$/;
|
|
21
|
+
const TOTAL_LINE_RE = /^Total\s+(\d+)\s+(\d+)/;
|
|
22
|
+
export function parseHeapHisto(text) {
|
|
23
|
+
const lines = text.replace(/\r\n/g, "\n").split("\n");
|
|
24
|
+
const entries = [];
|
|
25
|
+
let totalInstances = 0;
|
|
26
|
+
let totalBytes = 0;
|
|
27
|
+
for (const line of lines) {
|
|
28
|
+
const totalMatch = TOTAL_LINE_RE.exec(line);
|
|
29
|
+
if (totalMatch) {
|
|
30
|
+
totalInstances = parseInt(totalMatch[1], 10);
|
|
31
|
+
totalBytes = parseInt(totalMatch[2], 10);
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
const match = HISTO_LINE_RE.exec(line);
|
|
35
|
+
if (match) {
|
|
36
|
+
entries.push({
|
|
37
|
+
rank: parseInt(match[1], 10),
|
|
38
|
+
instances: parseInt(match[2], 10),
|
|
39
|
+
bytes: parseInt(match[3], 10),
|
|
40
|
+
className: match[4].trim(),
|
|
41
|
+
module: match[5] || null,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (totalInstances === 0 && entries.length > 0) {
|
|
46
|
+
totalInstances = entries.reduce((sum, e) => sum + e.instances, 0);
|
|
47
|
+
totalBytes = entries.reduce((sum, e) => sum + e.bytes, 0);
|
|
48
|
+
}
|
|
49
|
+
const issues = [];
|
|
50
|
+
const recommendations = [];
|
|
51
|
+
if (entries.length === 0) {
|
|
52
|
+
issues.push({
|
|
53
|
+
severity: "CRITICAL",
|
|
54
|
+
message: "No histogram entries found — input may not be a valid jmap -histo output",
|
|
55
|
+
className: "",
|
|
56
|
+
});
|
|
57
|
+
return { entries, totalInstances, totalBytes, issues, recommendations };
|
|
58
|
+
}
|
|
59
|
+
// Analyze top entries for suspicious patterns
|
|
60
|
+
for (const entry of entries.slice(0, 30)) {
|
|
61
|
+
const pctBytes = totalBytes > 0 ? (entry.bytes / totalBytes) * 100 : 0;
|
|
62
|
+
const isJdkInternal = JDK_INTERNALS.has(entry.className);
|
|
63
|
+
// Large non-JDK class consuming > 10% of heap
|
|
64
|
+
if (pctBytes > 10 && !isJdkInternal) {
|
|
65
|
+
issues.push({
|
|
66
|
+
severity: "CRITICAL",
|
|
67
|
+
message: `${entry.className} consumes ${pctBytes.toFixed(1)}% of heap (${formatBytes(entry.bytes)}, ${entry.instances} instances) — potential memory leak`,
|
|
68
|
+
className: entry.className,
|
|
69
|
+
});
|
|
70
|
+
recommendations.push(`Investigate ${entry.className} — use Eclipse MAT or VisualVM to trace retention paths. Check for unbounded caches or collections.`);
|
|
71
|
+
}
|
|
72
|
+
// Very high instance count for non-JDK class (> 100K)
|
|
73
|
+
if (entry.instances > 100_000 && !isJdkInternal && pctBytes <= 10) {
|
|
74
|
+
issues.push({
|
|
75
|
+
severity: "WARNING",
|
|
76
|
+
message: `${entry.className} has ${entry.instances.toLocaleString()} instances (${formatBytes(entry.bytes)}) — may indicate an object creation hotspot`,
|
|
77
|
+
className: entry.className,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
// Finalizer objects indicate slow finalization
|
|
81
|
+
if (entry.className === "java.lang.ref.Finalizer" && entry.instances > 10_000) {
|
|
82
|
+
issues.push({
|
|
83
|
+
severity: "WARNING",
|
|
84
|
+
message: `${entry.instances.toLocaleString()} Finalizer objects — finalization queue may be backed up. Classes using finalize() are blocking GC.`,
|
|
85
|
+
className: entry.className,
|
|
86
|
+
});
|
|
87
|
+
recommendations.push("Replace finalize() with Cleaner or try-with-resources. Finalizers delay GC and can cause memory pressure.");
|
|
88
|
+
}
|
|
89
|
+
// Char arrays ([C) or byte arrays ([B) dominating — usually String-related
|
|
90
|
+
if ((entry.className === "[B" || entry.className === "[C") && pctBytes > 40) {
|
|
91
|
+
issues.push({
|
|
92
|
+
severity: "WARNING",
|
|
93
|
+
message: `${entry.className === "[B" ? "Byte" : "Char"} arrays consume ${pctBytes.toFixed(1)}% of heap — likely driven by String retention. Check for large string caches or log buffering.`,
|
|
94
|
+
className: entry.className,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// Check for class loader leak indicators
|
|
99
|
+
const classCount = entries.find(e => e.className === "java.lang.Class");
|
|
100
|
+
if (classCount && classCount.instances > 30_000) {
|
|
101
|
+
issues.push({
|
|
102
|
+
severity: "WARNING",
|
|
103
|
+
message: `${classCount.instances.toLocaleString()} loaded classes — possible classloader leak (hot redeploy, dynamic proxy generation)`,
|
|
104
|
+
className: "java.lang.Class",
|
|
105
|
+
});
|
|
106
|
+
recommendations.push("Check for classloader leaks if using hot deployment (Tomcat, Spring DevTools). Consider restarting instead of redeploying.");
|
|
107
|
+
}
|
|
108
|
+
if (issues.length === 0) {
|
|
109
|
+
recommendations.push("Heap histogram looks healthy. For deeper analysis, capture a full heap dump: jmap -dump:live,format=b,file=heap.hprof <pid>");
|
|
110
|
+
}
|
|
111
|
+
return { entries, totalInstances, totalBytes, issues, recommendations };
|
|
112
|
+
}
|
|
113
|
+
function formatBytes(bytes) {
|
|
114
|
+
if (bytes >= 1_073_741_824)
|
|
115
|
+
return `${(bytes / 1_073_741_824).toFixed(1)} GB`;
|
|
116
|
+
if (bytes >= 1_048_576)
|
|
117
|
+
return `${(bytes / 1_048_576).toFixed(1)} MB`;
|
|
118
|
+
if (bytes >= 1024)
|
|
119
|
+
return `${(bytes / 1024).toFixed(0)} KB`;
|
|
120
|
+
return `${bytes} B`;
|
|
121
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parser for `jfr summary <file>` output.
|
|
3
|
+
*
|
|
4
|
+
* The jfr summary command prints event statistics from a JDK Flight Recorder
|
|
5
|
+
* (.jfr) file: event types, counts, and sizes. This parser extracts that data
|
|
6
|
+
* and produces analytical insights.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Parse `jfr summary` output text.
|
|
10
|
+
*
|
|
11
|
+
* Handles both the tabular format:
|
|
12
|
+
* Event Type Count Size (bytes)
|
|
13
|
+
* ===========================================================
|
|
14
|
+
* jdk.ObjectAllocationInNewTLAB 542 28184
|
|
15
|
+
*
|
|
16
|
+
* And the summary lines at the top (start time, duration, etc.).
|
|
17
|
+
*/
|
|
18
|
+
export function parseJfrSummary(text) {
|
|
19
|
+
if (!text || text.trim().length === 0) {
|
|
20
|
+
throw new Error("Empty JFR summary input");
|
|
21
|
+
}
|
|
22
|
+
const lines = text.split("\n");
|
|
23
|
+
const events = [];
|
|
24
|
+
let startTime;
|
|
25
|
+
let duration;
|
|
26
|
+
// Match event rows: event_name count size
|
|
27
|
+
// Format: " jdk.GCPhasePause 123 45678"
|
|
28
|
+
const eventLineRegex = /^\s*([\w.]+(?:\s+\([^)]+\))?)\s+(\d+)\s+(\d+)\s*$/;
|
|
29
|
+
// Alternative format without size column
|
|
30
|
+
const eventNoSizeRegex = /^\s*([\w.]+)\s+(\d+)\s*$/;
|
|
31
|
+
// Header metadata patterns
|
|
32
|
+
const startTimeRegex = /start\s*(?:time)?[:=]\s*(.+)/i;
|
|
33
|
+
const durationRegex = /duration[:=]\s*(.+)/i;
|
|
34
|
+
for (const line of lines) {
|
|
35
|
+
const trimmed = line.trim();
|
|
36
|
+
// Skip empty lines, headers, separators
|
|
37
|
+
if (!trimmed || /^[=\-]+$/.test(trimmed) || /^Event\s+Type/i.test(trimmed)) {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
// Check for metadata lines
|
|
41
|
+
const startMatch = trimmed.match(startTimeRegex);
|
|
42
|
+
if (startMatch) {
|
|
43
|
+
startTime = startMatch[1].trim();
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
const durMatch = trimmed.match(durationRegex);
|
|
47
|
+
if (durMatch) {
|
|
48
|
+
duration = durMatch[1].trim();
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
// Parse event lines (with size)
|
|
52
|
+
const eventMatch = line.match(eventLineRegex);
|
|
53
|
+
if (eventMatch) {
|
|
54
|
+
events.push({
|
|
55
|
+
name: eventMatch[1].trim(),
|
|
56
|
+
count: parseInt(eventMatch[2], 10),
|
|
57
|
+
size: parseInt(eventMatch[3], 10),
|
|
58
|
+
});
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
// Parse event lines (without size — some JFR versions)
|
|
62
|
+
const noSizeMatch = line.match(eventNoSizeRegex);
|
|
63
|
+
if (noSizeMatch && !trimmed.startsWith("#") && !/^[A-Z][a-z]+:/.test(trimmed)) {
|
|
64
|
+
events.push({
|
|
65
|
+
name: noSizeMatch[1].trim(),
|
|
66
|
+
count: parseInt(noSizeMatch[2], 10),
|
|
67
|
+
size: 0,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (events.length === 0) {
|
|
72
|
+
throw new Error("No JFR events found in summary. Ensure input is from `jfr summary <file>`.");
|
|
73
|
+
}
|
|
74
|
+
const totalEvents = events.reduce((sum, e) => sum + e.count, 0);
|
|
75
|
+
const totalSize = events.reduce((sum, e) => sum + e.size, 0);
|
|
76
|
+
const { issues, recommendations } = analyzeJfrEvents(events, totalEvents, totalSize);
|
|
77
|
+
return {
|
|
78
|
+
events,
|
|
79
|
+
totalEvents,
|
|
80
|
+
totalSize,
|
|
81
|
+
startTime,
|
|
82
|
+
duration,
|
|
83
|
+
issues,
|
|
84
|
+
recommendations,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
function analyzeJfrEvents(events, totalEvents, totalSize) {
|
|
88
|
+
const issues = [];
|
|
89
|
+
const recommendations = [];
|
|
90
|
+
// Build a lookup by event name
|
|
91
|
+
const byName = new Map(events.map((e) => [e.name, e]));
|
|
92
|
+
// Check for excessive GC events
|
|
93
|
+
const gcEvents = events.filter((e) => e.name.startsWith("jdk.GC") || e.name.startsWith("jdk.G1") || e.name.startsWith("jdk.ZGC"));
|
|
94
|
+
const gcCount = gcEvents.reduce((sum, e) => sum + e.count, 0);
|
|
95
|
+
if (gcCount > 1000) {
|
|
96
|
+
issues.push(`High GC activity: ${gcCount.toLocaleString()} GC events recorded. Possible memory pressure.`);
|
|
97
|
+
recommendations.push("Analyze GC logs with `analyze_gc_log` for pause time breakdown and tuning recommendations.");
|
|
98
|
+
}
|
|
99
|
+
// Check for excessive allocation events
|
|
100
|
+
const allocTLAB = byName.get("jdk.ObjectAllocationInNewTLAB");
|
|
101
|
+
const allocOutside = byName.get("jdk.ObjectAllocationOutsideTLAB");
|
|
102
|
+
if (allocOutside && allocOutside.count > 100) {
|
|
103
|
+
issues.push(`${allocOutside.count.toLocaleString()} allocations outside TLAB — objects too large for thread-local allocation buffers.`);
|
|
104
|
+
recommendations.push("Review large object allocations. Consider increasing TLAB size with -XX:TLABSize or reducing object sizes.");
|
|
105
|
+
}
|
|
106
|
+
// Check for thread contention
|
|
107
|
+
const monitorEnter = byName.get("jdk.JavaMonitorEnter");
|
|
108
|
+
const monitorWait = byName.get("jdk.JavaMonitorWait");
|
|
109
|
+
if (monitorEnter && monitorEnter.count > 500) {
|
|
110
|
+
issues.push(`${monitorEnter.count.toLocaleString()} monitor enter events — significant lock contention.`);
|
|
111
|
+
recommendations.push("Use `analyze_thread_dump` to identify contention hotspots and consider lock-free alternatives.");
|
|
112
|
+
}
|
|
113
|
+
// Check for thread starts (churn)
|
|
114
|
+
const threadStart = byName.get("jdk.ThreadStart");
|
|
115
|
+
if (threadStart && threadStart.count > 200) {
|
|
116
|
+
issues.push(`${threadStart.count.toLocaleString()} threads started — possible thread churn. Consider thread pooling.`);
|
|
117
|
+
}
|
|
118
|
+
// Check for exception events
|
|
119
|
+
const exceptions = byName.get("jdk.JavaExceptionThrow");
|
|
120
|
+
if (exceptions && exceptions.count > 500) {
|
|
121
|
+
issues.push(`${exceptions.count.toLocaleString()} exceptions thrown — exceptions are expensive and may indicate control flow issues.`);
|
|
122
|
+
recommendations.push("Review exception handling patterns. Avoid using exceptions for control flow.");
|
|
123
|
+
}
|
|
124
|
+
// Check for class loading
|
|
125
|
+
const classLoad = byName.get("jdk.ClassLoad");
|
|
126
|
+
if (classLoad && classLoad.count > 1000) {
|
|
127
|
+
issues.push(`${classLoad.count.toLocaleString()} class loads — excessive class loading may indicate classloader leak or dynamic proxy overuse.`);
|
|
128
|
+
}
|
|
129
|
+
// Check for file/socket I/O
|
|
130
|
+
const fileRead = byName.get("jdk.FileRead");
|
|
131
|
+
const fileWrite = byName.get("jdk.FileWrite");
|
|
132
|
+
const socketRead = byName.get("jdk.SocketRead");
|
|
133
|
+
const socketWrite = byName.get("jdk.SocketWrite");
|
|
134
|
+
const ioCount = [fileRead, fileWrite, socketRead, socketWrite]
|
|
135
|
+
.filter(Boolean)
|
|
136
|
+
.reduce((sum, e) => sum + e.count, 0);
|
|
137
|
+
if (ioCount > 5000) {
|
|
138
|
+
issues.push(`High I/O activity: ${ioCount.toLocaleString()} file/socket events. Consider batching or buffering.`);
|
|
139
|
+
}
|
|
140
|
+
// Check for compilation events
|
|
141
|
+
const compilation = byName.get("jdk.Compilation");
|
|
142
|
+
if (compilation && compilation.count > 500) {
|
|
143
|
+
issues.push(`${compilation.count.toLocaleString()} JIT compilations — may indicate insufficient code cache or deoptimizations.`);
|
|
144
|
+
recommendations.push("Check -XX:ReservedCodeCacheSize and look for deoptimization events.");
|
|
145
|
+
}
|
|
146
|
+
// Recording size analysis
|
|
147
|
+
if (totalSize > 100_000_000) {
|
|
148
|
+
recommendations.push(`Recording is ${(totalSize / 1_048_576).toFixed(0)} MB. Consider narrowing event filters with jfc settings.`);
|
|
149
|
+
}
|
|
150
|
+
// If dominant event type uses >50% of total
|
|
151
|
+
if (events.length > 0) {
|
|
152
|
+
const sorted = [...events].sort((a, b) => b.count - a.count);
|
|
153
|
+
const topEvent = sorted[0];
|
|
154
|
+
const topPct = totalEvents > 0 ? (topEvent.count / totalEvents) * 100 : 0;
|
|
155
|
+
if (topPct > 50) {
|
|
156
|
+
recommendations.push(`Event "${topEvent.name}" dominates at ${topPct.toFixed(0)}% of all events. Focus analysis there.`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return { issues, recommendations };
|
|
160
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HotSpot thread dump parser.
|
|
3
|
+
*
|
|
4
|
+
* Parses jstack/kill -3 output into structured thread data including:
|
|
5
|
+
* - Thread name, state, daemon status
|
|
6
|
+
* - Stack traces
|
|
7
|
+
* - Lock information (waiting on, holding)
|
|
8
|
+
*/
|
|
9
|
+
// Java 21+ thread dumps include [os_thread_id] after #id:
|
|
10
|
+
// "Finalizer" #11 [2523503] daemon prio=8 os_prio=0 ...
|
|
11
|
+
const THREAD_HEADER_RE = /^"(.+?)"\s*(#\d+)?\s*(?:\[\d+\])?\s*(daemon)?\s*(?:prio=(\d+))?\s*(?:os_prio=\d+)?\s*(?:cpu=[\d.]+ms)?\s*(?:elapsed=[\d.]+s)?\s*(?:tid=(0x[0-9a-f]+))?\s*(?:nid=(0x[0-9a-f]+|\d+))?\s*(.*)/;
|
|
12
|
+
const STATE_RE = /java\.lang\.Thread\.State:\s*(\S+)/;
|
|
13
|
+
const WAITING_ON_RE = /- waiting to lock\s+<(0x[0-9a-f]+)>/;
|
|
14
|
+
const LOCKED_RE = /- locked\s+<(0x[0-9a-f]+)>/;
|
|
15
|
+
const PARKING_RE = /- parking to wait for\s+<(0x[0-9a-f]+)>/;
|
|
16
|
+
const WAITING_OBJ_RE = /- waiting on\s+<(0x[0-9a-f]+)>/;
|
|
17
|
+
export function parseThreadDump(text) {
|
|
18
|
+
const lines = text.replace(/\r\n/g, "\n").split("\n");
|
|
19
|
+
const threads = [];
|
|
20
|
+
let jvmInfo = "";
|
|
21
|
+
let timestamp = "";
|
|
22
|
+
// Extract JVM info and timestamp from header
|
|
23
|
+
for (const line of lines) {
|
|
24
|
+
if (line.startsWith("Full thread dump")) {
|
|
25
|
+
jvmInfo = line.replace("Full thread dump ", "").replace(":", "").trim();
|
|
26
|
+
break;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
// Look for timestamp (common format: YYYY-MM-DD HH:MM:SS)
|
|
30
|
+
const tsMatch = text.match(/(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})/);
|
|
31
|
+
if (tsMatch) {
|
|
32
|
+
timestamp = tsMatch[1];
|
|
33
|
+
}
|
|
34
|
+
let currentThread = null;
|
|
35
|
+
let inStack = false;
|
|
36
|
+
for (const line of lines) {
|
|
37
|
+
// Try matching thread header
|
|
38
|
+
const headerMatch = line.match(THREAD_HEADER_RE);
|
|
39
|
+
if (headerMatch && line.startsWith('"')) {
|
|
40
|
+
// Save previous thread
|
|
41
|
+
if (currentThread) {
|
|
42
|
+
threads.push(currentThread);
|
|
43
|
+
}
|
|
44
|
+
currentThread = {
|
|
45
|
+
name: headerMatch[1],
|
|
46
|
+
state: "UNKNOWN",
|
|
47
|
+
isDaemon: headerMatch[3] === "daemon",
|
|
48
|
+
priority: headerMatch[4] ? parseInt(headerMatch[4], 10) : 5,
|
|
49
|
+
tid: headerMatch[5] || "",
|
|
50
|
+
nid: headerMatch[6] || "",
|
|
51
|
+
stackTrace: [],
|
|
52
|
+
waitingOn: null,
|
|
53
|
+
holdsLocks: [],
|
|
54
|
+
blockedBy: null,
|
|
55
|
+
};
|
|
56
|
+
// Some thread headers include state inline (e.g., "runnable", "waiting on condition")
|
|
57
|
+
const inlineState = headerMatch[7]?.trim();
|
|
58
|
+
if (inlineState) {
|
|
59
|
+
if (inlineState.includes("runnable"))
|
|
60
|
+
currentThread.state = "RUNNABLE";
|
|
61
|
+
else if (inlineState.includes("waiting on condition"))
|
|
62
|
+
currentThread.state = "TIMED_WAITING";
|
|
63
|
+
else if (inlineState.includes("waiting for monitor"))
|
|
64
|
+
currentThread.state = "BLOCKED";
|
|
65
|
+
else if (inlineState.includes("in Object.wait"))
|
|
66
|
+
currentThread.state = "WAITING";
|
|
67
|
+
else if (inlineState.includes("sleeping"))
|
|
68
|
+
currentThread.state = "TIMED_WAITING";
|
|
69
|
+
}
|
|
70
|
+
inStack = true;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (!currentThread || !inStack)
|
|
74
|
+
continue;
|
|
75
|
+
// Thread state line
|
|
76
|
+
const stateMatch = line.match(STATE_RE);
|
|
77
|
+
if (stateMatch) {
|
|
78
|
+
currentThread.state = stateMatch[1];
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
// Stack trace line
|
|
82
|
+
if (line.match(/^\s+at\s+/)) {
|
|
83
|
+
currentThread.stackTrace.push(line.trim());
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
// Lock information
|
|
87
|
+
const waitMatch = line.match(WAITING_ON_RE);
|
|
88
|
+
if (waitMatch) {
|
|
89
|
+
currentThread.waitingOn = waitMatch[1];
|
|
90
|
+
currentThread.blockedBy = waitMatch[1];
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
const lockMatch = line.match(LOCKED_RE);
|
|
94
|
+
if (lockMatch) {
|
|
95
|
+
currentThread.holdsLocks.push(lockMatch[1]);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
const parkMatch = line.match(PARKING_RE);
|
|
99
|
+
if (parkMatch) {
|
|
100
|
+
currentThread.waitingOn = parkMatch[1];
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
const waitObjMatch = line.match(WAITING_OBJ_RE);
|
|
104
|
+
if (waitObjMatch) {
|
|
105
|
+
currentThread.waitingOn = waitObjMatch[1];
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
// Empty line ends the current thread's stack
|
|
109
|
+
if (line.trim() === "" && currentThread.stackTrace.length > 0) {
|
|
110
|
+
inStack = false;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
// Push last thread
|
|
114
|
+
if (currentThread) {
|
|
115
|
+
threads.push(currentThread);
|
|
116
|
+
}
|
|
117
|
+
return { jvmInfo, timestamp, threads };
|
|
118
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "mcp-jvm-diagnostics",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MCP server for JVM diagnostics — analyze thread dumps, detect deadlocks, parse GC logs, and get JVM tuning recommendations",
|
|
5
|
+
"author": "Dmytro Lisnichenko",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./build/index.js",
|
|
8
|
+
"bin": {
|
|
9
|
+
"mcp-jvm-diagnostics": "./build/index.js"
|
|
10
|
+
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"build": "tsc && chmod 755 build/index.js",
|
|
13
|
+
"test": "vitest run",
|
|
14
|
+
"dev": "tsc --watch",
|
|
15
|
+
"start": "node build/index.js",
|
|
16
|
+
"prepublishOnly": "npm run build && npm test"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"build/**/*.js",
|
|
20
|
+
"!build/__tests__",
|
|
21
|
+
"README.md",
|
|
22
|
+
"LICENSE"
|
|
23
|
+
],
|
|
24
|
+
"keywords": [
|
|
25
|
+
"mcp",
|
|
26
|
+
"model-context-protocol",
|
|
27
|
+
"jvm",
|
|
28
|
+
"java",
|
|
29
|
+
"thread-dump",
|
|
30
|
+
"deadlock",
|
|
31
|
+
"gc-log",
|
|
32
|
+
"garbage-collection",
|
|
33
|
+
"diagnostics",
|
|
34
|
+
"performance",
|
|
35
|
+
"ai",
|
|
36
|
+
"claude"
|
|
37
|
+
],
|
|
38
|
+
"license": "MIT",
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=18.0.0"
|
|
41
|
+
},
|
|
42
|
+
"repository": {
|
|
43
|
+
"type": "git",
|
|
44
|
+
"url": "https://github.com/Dmitriusan/mcp-jvm-diagnostics"
|
|
45
|
+
},
|
|
46
|
+
"homepage": "https://github.com/Dmitriusan/mcp-jvm-diagnostics#readme",
|
|
47
|
+
"bugs": {
|
|
48
|
+
"url": "https://github.com/Dmitriusan/mcp-jvm-diagnostics/issues"
|
|
49
|
+
},
|
|
50
|
+
"dependencies": {
|
|
51
|
+
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
52
|
+
"zod": "^3.24.2"
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"@types/node": "^22.0.0",
|
|
56
|
+
"typescript": "^5.8.2",
|
|
57
|
+
"vitest": "^4.0.18"
|
|
58
|
+
}
|
|
59
|
+
}
|