javaperf 1.0.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/.cursor/mcp.json +12 -0
- package/README.md +185 -0
- package/README_RU.md +185 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +255 -0
- package/dist/tools/analyze_threads.d.ts +13 -0
- package/dist/tools/analyze_threads.js +26 -0
- package/dist/tools/heap_dump.d.ts +10 -0
- package/dist/tools/heap_dump.js +29 -0
- package/dist/tools/heap_histogram.d.ts +16 -0
- package/dist/tools/heap_histogram.js +30 -0
- package/dist/tools/heap_info.d.ts +10 -0
- package/dist/tools/heap_info.js +10 -0
- package/dist/tools/list_procs.d.ts +10 -0
- package/dist/tools/list_procs.js +11 -0
- package/dist/tools/parse_jfr.d.ts +16 -0
- package/dist/tools/parse_jfr.js +64 -0
- package/dist/tools/profile_frequency.d.ts +13 -0
- package/dist/tools/profile_frequency.js +39 -0
- package/dist/tools/profile_memory.d.ts +13 -0
- package/dist/tools/profile_memory.js +67 -0
- package/dist/tools/profile_time.d.ts +13 -0
- package/dist/tools/profile_time.js +38 -0
- package/dist/tools/start_profiling.d.ts +13 -0
- package/dist/tools/start_profiling.js +36 -0
- package/dist/tools/stop_profiling.d.ts +13 -0
- package/dist/tools/stop_profiling.js +30 -0
- package/dist/tools/trace_method.d.ts +22 -0
- package/dist/tools/trace_method.js +58 -0
- package/dist/tools/vm_info.d.ts +10 -0
- package/dist/tools/vm_info.js +19 -0
- package/dist/utils/jdk.d.ts +17 -0
- package/dist/utils/jdk.js +133 -0
- package/dist/utils/jfr-json.d.ts +28 -0
- package/dist/utils/jfr-json.js +46 -0
- package/dist/utils/paths.d.ts +5 -0
- package/dist/utils/paths.js +13 -0
- package/eslint.config.js +23 -0
- package/package.json +29 -0
- package/src/index.ts +322 -0
- package/src/tools/analyze_threads.ts +30 -0
- package/src/tools/heap_dump.ts +42 -0
- package/src/tools/heap_histogram.ts +43 -0
- package/src/tools/heap_info.ts +14 -0
- package/src/tools/list_procs.ts +15 -0
- package/src/tools/parse_jfr.ts +75 -0
- package/src/tools/profile_frequency.ts +48 -0
- package/src/tools/profile_memory.ts +82 -0
- package/src/tools/profile_time.ts +47 -0
- package/src/tools/start_profiling.ts +54 -0
- package/src/tools/stop_profiling.ts +42 -0
- package/src/tools/trace_method.ts +72 -0
- package/src/tools/vm_info.ts +26 -0
- package/src/utils/jdk.ts +149 -0
- package/src/utils/jfr-json.ts +55 -0
- package/src/utils/paths.ts +13 -0
- package/tsconfig.json +15 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const heapDumpSchema: z.ZodObject<{
|
|
3
|
+
pid: z.ZodNumber;
|
|
4
|
+
}, "strip", z.ZodTypeAny, {
|
|
5
|
+
pid: number;
|
|
6
|
+
}, {
|
|
7
|
+
pid: number;
|
|
8
|
+
}>;
|
|
9
|
+
export type HeapDumpInput = z.infer<typeof heapDumpSchema>;
|
|
10
|
+
export declare function heapDump(input: HeapDumpInput): Promise<string>;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, statSync } from "node:fs";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { runJcmd } from "../utils/jdk.js";
|
|
4
|
+
import { RECORDINGS_DIR } from "../utils/paths.js";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
export const heapDumpSchema = z.object({
|
|
7
|
+
pid: z.number().int().positive(),
|
|
8
|
+
});
|
|
9
|
+
const HEAP_DUMP_PATH = join(RECORDINGS_DIR, "heap_dump.hprof");
|
|
10
|
+
export async function heapDump(input) {
|
|
11
|
+
const { pid } = input;
|
|
12
|
+
if (!existsSync(RECORDINGS_DIR)) {
|
|
13
|
+
mkdirSync(RECORDINGS_DIR, { recursive: true });
|
|
14
|
+
}
|
|
15
|
+
runJcmd(pid, "GC.heap_dump", [`filename=${HEAP_DUMP_PATH}`]);
|
|
16
|
+
if (!existsSync(HEAP_DUMP_PATH)) {
|
|
17
|
+
return JSON.stringify({
|
|
18
|
+
status: "error",
|
|
19
|
+
message: "Heap dump command completed but file was not found.",
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
const stats = statSync(HEAP_DUMP_PATH);
|
|
23
|
+
return JSON.stringify({
|
|
24
|
+
filepath: HEAP_DUMP_PATH,
|
|
25
|
+
fileSize: stats.size,
|
|
26
|
+
status: "saved",
|
|
27
|
+
hint: "Open in Eclipse MAT, VisualVM, or JProfiler for analysis. File can be large (hundreds of MB - GB).",
|
|
28
|
+
}, null, 2);
|
|
29
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const heapHistogramSchema: z.ZodObject<{
|
|
3
|
+
pid: z.ZodNumber;
|
|
4
|
+
topN: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
|
|
5
|
+
all: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
6
|
+
}, "strip", z.ZodTypeAny, {
|
|
7
|
+
topN: number;
|
|
8
|
+
pid: number;
|
|
9
|
+
all: boolean;
|
|
10
|
+
}, {
|
|
11
|
+
pid: number;
|
|
12
|
+
topN?: number | undefined;
|
|
13
|
+
all?: boolean | undefined;
|
|
14
|
+
}>;
|
|
15
|
+
export type HeapHistogramInput = z.infer<typeof heapHistogramSchema>;
|
|
16
|
+
export declare function heapHistogram(input: HeapHistogramInput): Promise<string>;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { runJcmd } from "../utils/jdk.js";
|
|
3
|
+
export const heapHistogramSchema = z.object({
|
|
4
|
+
pid: z.number().int().positive(),
|
|
5
|
+
topN: z.number().int().min(1).max(200).optional().default(20),
|
|
6
|
+
all: z.boolean().optional().default(false),
|
|
7
|
+
});
|
|
8
|
+
function parseHistogramOutput(output) {
|
|
9
|
+
const lines = output.trim().split("\n");
|
|
10
|
+
const result = [];
|
|
11
|
+
for (const line of lines) {
|
|
12
|
+
const match = line.match(/^\s*\d+:\s+(\d+)\s+(\d+)\s+(.+)$/);
|
|
13
|
+
if (!match)
|
|
14
|
+
continue;
|
|
15
|
+
const instances = parseInt(match[1], 10);
|
|
16
|
+
const bytes = parseInt(match[2], 10);
|
|
17
|
+
const className = match[3].trim();
|
|
18
|
+
result.push({ className, instances, bytes });
|
|
19
|
+
}
|
|
20
|
+
result.sort((a, b) => b.bytes - a.bytes);
|
|
21
|
+
return result;
|
|
22
|
+
}
|
|
23
|
+
export async function heapHistogram(input) {
|
|
24
|
+
const { pid, topN, all } = input;
|
|
25
|
+
const opts = all ? ["-all"] : [];
|
|
26
|
+
const output = runJcmd(pid, "GC.class_histogram", opts);
|
|
27
|
+
const entries = parseHistogramOutput(output);
|
|
28
|
+
const top = entries.slice(0, topN);
|
|
29
|
+
return JSON.stringify({ entries: top, total: entries.length }, null, 2);
|
|
30
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const heapInfoSchema: z.ZodObject<{
|
|
3
|
+
pid: z.ZodNumber;
|
|
4
|
+
}, "strip", z.ZodTypeAny, {
|
|
5
|
+
pid: number;
|
|
6
|
+
}, {
|
|
7
|
+
pid: number;
|
|
8
|
+
}>;
|
|
9
|
+
export type HeapInfoInput = z.infer<typeof heapInfoSchema>;
|
|
10
|
+
export declare function heapInfo(input: HeapInfoInput): Promise<string>;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { runJcmd } from "../utils/jdk.js";
|
|
3
|
+
export const heapInfoSchema = z.object({
|
|
4
|
+
pid: z.number().int().positive(),
|
|
5
|
+
});
|
|
6
|
+
export async function heapInfo(input) {
|
|
7
|
+
const { pid } = input;
|
|
8
|
+
const output = runJcmd(pid, "GC.heap_info");
|
|
9
|
+
return output.trim();
|
|
10
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const listJavaProcessesSchema: z.ZodObject<{
|
|
3
|
+
topN: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
|
|
4
|
+
}, "strip", z.ZodTypeAny, {
|
|
5
|
+
topN: number;
|
|
6
|
+
}, {
|
|
7
|
+
topN?: number | undefined;
|
|
8
|
+
}>;
|
|
9
|
+
export type ListJavaProcessesInput = z.infer<typeof listJavaProcessesSchema>;
|
|
10
|
+
export declare function listJavaProcesses(input: ListJavaProcessesInput): Promise<string>;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { runJps } from "../utils/jdk.js";
|
|
3
|
+
export const listJavaProcessesSchema = z.object({
|
|
4
|
+
topN: z.number().int().min(1).max(100).optional().default(10),
|
|
5
|
+
});
|
|
6
|
+
export async function listJavaProcesses(input) {
|
|
7
|
+
const { topN } = input;
|
|
8
|
+
const processes = runJps();
|
|
9
|
+
const limited = processes.slice(0, topN);
|
|
10
|
+
return JSON.stringify(limited, null, 2);
|
|
11
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const parseJfrSummarySchema: z.ZodObject<{
|
|
3
|
+
filepath: z.ZodString;
|
|
4
|
+
events: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
5
|
+
topN: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
|
|
6
|
+
}, "strip", z.ZodTypeAny, {
|
|
7
|
+
topN: number;
|
|
8
|
+
filepath: string;
|
|
9
|
+
events?: string[] | undefined;
|
|
10
|
+
}, {
|
|
11
|
+
filepath: string;
|
|
12
|
+
topN?: number | undefined;
|
|
13
|
+
events?: string[] | undefined;
|
|
14
|
+
}>;
|
|
15
|
+
export type ParseJfrSummaryInput = z.infer<typeof parseJfrSummarySchema>;
|
|
16
|
+
export declare function parseJfrSummary(input: ParseJfrSummaryInput): Promise<string>;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { runJfr } from "../utils/jdk.js";
|
|
4
|
+
import { resolveProfilePath } from "../utils/paths.js";
|
|
5
|
+
import { getEvents, getEventType, getStackTrace, getMethodKey } from "../utils/jfr-json.js";
|
|
6
|
+
export const parseJfrSummarySchema = z.object({
|
|
7
|
+
filepath: z.string(),
|
|
8
|
+
events: z.array(z.string()).optional(),
|
|
9
|
+
topN: z.number().int().min(1).max(100).optional().default(10),
|
|
10
|
+
});
|
|
11
|
+
export async function parseJfrSummary(input) {
|
|
12
|
+
const { topN } = input;
|
|
13
|
+
const filepath = resolveProfilePath(input.filepath);
|
|
14
|
+
if (!existsSync(filepath)) {
|
|
15
|
+
return JSON.stringify({ error: `File not found: ${filepath}` });
|
|
16
|
+
}
|
|
17
|
+
const events = input.events ?? [
|
|
18
|
+
"jdk.ExecutionSample",
|
|
19
|
+
"jdk.GarbageCollection",
|
|
20
|
+
"jdk.JavaThreadStatistics",
|
|
21
|
+
"jdk.ThreadAllocationStatistics",
|
|
22
|
+
];
|
|
23
|
+
const eventsArg = events.join(",");
|
|
24
|
+
const [summaryOut, jsonOut] = await Promise.all([
|
|
25
|
+
runJfr(["summary", filepath]),
|
|
26
|
+
runJfr(["print", "--json", "--events", eventsArg, filepath]),
|
|
27
|
+
]);
|
|
28
|
+
const methodCount = new Map();
|
|
29
|
+
let gcCount = 0;
|
|
30
|
+
const anomalies = [];
|
|
31
|
+
try {
|
|
32
|
+
const parsed = JSON.parse(jsonOut);
|
|
33
|
+
const eventsList = getEvents(parsed);
|
|
34
|
+
for (const ev of eventsList) {
|
|
35
|
+
const typ = getEventType(ev);
|
|
36
|
+
if (typ === "jdk.GarbageCollection")
|
|
37
|
+
gcCount++;
|
|
38
|
+
if (typ === "jdk.ExecutionSample") {
|
|
39
|
+
const frames = getStackTrace(ev)?.frames ?? [];
|
|
40
|
+
for (const f of frames) {
|
|
41
|
+
const key = getMethodKey(f);
|
|
42
|
+
if (key)
|
|
43
|
+
methodCount.set(key, (methodCount.get(key) ?? 0) + 1);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (gcCount > 100)
|
|
48
|
+
anomalies.push("High GC count - possible memory pressure");
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
// continue with summary only
|
|
52
|
+
}
|
|
53
|
+
const topMethods = [...methodCount.entries()]
|
|
54
|
+
.sort((a, b) => b[1] - a[1])
|
|
55
|
+
.slice(0, topN)
|
|
56
|
+
.map(([m, c]) => ({ method: m, samples: c }));
|
|
57
|
+
const result = {
|
|
58
|
+
summary: summaryOut.trim(),
|
|
59
|
+
topMethods,
|
|
60
|
+
gcStats: { gcEvents: gcCount },
|
|
61
|
+
anomalies: anomalies.length ? anomalies : undefined,
|
|
62
|
+
};
|
|
63
|
+
return JSON.stringify(result, null, 2);
|
|
64
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const profileFrequencySchema: z.ZodObject<{
|
|
3
|
+
filepath: z.ZodString;
|
|
4
|
+
topN: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
|
|
5
|
+
}, "strip", z.ZodTypeAny, {
|
|
6
|
+
topN: number;
|
|
7
|
+
filepath: string;
|
|
8
|
+
}, {
|
|
9
|
+
filepath: string;
|
|
10
|
+
topN?: number | undefined;
|
|
11
|
+
}>;
|
|
12
|
+
export type ProfileFrequencyInput = z.infer<typeof profileFrequencySchema>;
|
|
13
|
+
export declare function profileFrequency(input: ProfileFrequencyInput): Promise<string>;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { runJfr } from "../utils/jdk.js";
|
|
4
|
+
import { resolveProfilePath } from "../utils/paths.js";
|
|
5
|
+
import { getEvents, getStackTrace, getMethodKey } from "../utils/jfr-json.js";
|
|
6
|
+
export const profileFrequencySchema = z.object({
|
|
7
|
+
filepath: z.string(),
|
|
8
|
+
topN: z.number().int().min(1).max(100).optional().default(10),
|
|
9
|
+
});
|
|
10
|
+
export async function profileFrequency(input) {
|
|
11
|
+
const { topN } = input;
|
|
12
|
+
const filepath = resolveProfilePath(input.filepath);
|
|
13
|
+
if (!existsSync(filepath)) {
|
|
14
|
+
return JSON.stringify({ error: `File not found: ${filepath}` });
|
|
15
|
+
}
|
|
16
|
+
const output = await runJfr(["print", "--json", "--events", "jdk.ExecutionSample", filepath]);
|
|
17
|
+
const leafCount = new Map();
|
|
18
|
+
try {
|
|
19
|
+
const parsed = JSON.parse(output);
|
|
20
|
+
const eventsList = getEvents(parsed);
|
|
21
|
+
for (const ev of eventsList) {
|
|
22
|
+
const frames = getStackTrace(ev)?.frames ?? [];
|
|
23
|
+
const leaf = frames[0];
|
|
24
|
+
if (leaf) {
|
|
25
|
+
const key = getMethodKey(leaf);
|
|
26
|
+
if (key)
|
|
27
|
+
leafCount.set(key, (leafCount.get(key) ?? 0) + 1);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return JSON.stringify({ error: "Failed to parse JFR ExecutionSample output." });
|
|
33
|
+
}
|
|
34
|
+
const top = [...leafCount.entries()]
|
|
35
|
+
.sort((a, b) => b[1] - a[1])
|
|
36
|
+
.slice(0, topN)
|
|
37
|
+
.map(([method, samples]) => ({ method, samples, note: "exclusive (leaf frame)" }));
|
|
38
|
+
return JSON.stringify({ profile: "frequency", topMethods: top }, null, 2);
|
|
39
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const profileMemorySchema: z.ZodObject<{
|
|
3
|
+
filepath: z.ZodString;
|
|
4
|
+
topN: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
|
|
5
|
+
}, "strip", z.ZodTypeAny, {
|
|
6
|
+
topN: number;
|
|
7
|
+
filepath: string;
|
|
8
|
+
}, {
|
|
9
|
+
filepath: string;
|
|
10
|
+
topN?: number | undefined;
|
|
11
|
+
}>;
|
|
12
|
+
export type ProfileMemoryInput = z.infer<typeof profileMemorySchema>;
|
|
13
|
+
export declare function profileMemory(input: ProfileMemoryInput): Promise<string>;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { runJfr } from "../utils/jdk.js";
|
|
4
|
+
import { resolveProfilePath } from "../utils/paths.js";
|
|
5
|
+
import { getEvents, getEventType, getStackTrace, getMethodKey } from "../utils/jfr-json.js";
|
|
6
|
+
export const profileMemorySchema = z.object({
|
|
7
|
+
filepath: z.string(),
|
|
8
|
+
topN: z.number().int().min(1).max(100).optional().default(10),
|
|
9
|
+
});
|
|
10
|
+
const MEMORY_EVENTS = [
|
|
11
|
+
"jdk.ObjectAllocationInNewTLAB",
|
|
12
|
+
"jdk.ObjectAllocationOutsideTLAB",
|
|
13
|
+
"jdk.ObjectAllocationSample",
|
|
14
|
+
"jdk.OldObjectSample",
|
|
15
|
+
"jdk.GarbageCollection",
|
|
16
|
+
"jdk.HeapSummary",
|
|
17
|
+
].join(",");
|
|
18
|
+
export async function profileMemory(input) {
|
|
19
|
+
const { topN } = input;
|
|
20
|
+
const filepath = resolveProfilePath(input.filepath);
|
|
21
|
+
if (!existsSync(filepath)) {
|
|
22
|
+
return JSON.stringify({ error: `File not found: ${filepath}` });
|
|
23
|
+
}
|
|
24
|
+
const output = await runJfr(["print", "--json", "--events", MEMORY_EVENTS, filepath]);
|
|
25
|
+
const allocatorCount = new Map();
|
|
26
|
+
let gcCount = 0;
|
|
27
|
+
const potentialLeaks = [];
|
|
28
|
+
try {
|
|
29
|
+
const parsed = JSON.parse(output);
|
|
30
|
+
const eventsList = getEvents(parsed);
|
|
31
|
+
for (const ev of eventsList) {
|
|
32
|
+
const typ = getEventType(ev);
|
|
33
|
+
if (typ === "jdk.GarbageCollection")
|
|
34
|
+
gcCount++;
|
|
35
|
+
const stackTrace = getStackTrace(ev);
|
|
36
|
+
const frames = stackTrace?.frames;
|
|
37
|
+
if ((typ === "jdk.ObjectAllocationInNewTLAB" ||
|
|
38
|
+
typ === "jdk.ObjectAllocationOutsideTLAB" ||
|
|
39
|
+
typ === "jdk.ObjectAllocationSample") &&
|
|
40
|
+
frames?.length) {
|
|
41
|
+
const top = frames[0];
|
|
42
|
+
const key = getMethodKey(top);
|
|
43
|
+
if (key && key !== "unknown")
|
|
44
|
+
allocatorCount.set(key, (allocatorCount.get(key) ?? 0) + 1);
|
|
45
|
+
}
|
|
46
|
+
if (typ === "jdk.OldObjectSample" && frames?.length) {
|
|
47
|
+
const top = frames[0];
|
|
48
|
+
const key = getMethodKey(top);
|
|
49
|
+
if (key)
|
|
50
|
+
potentialLeaks.push(key);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return JSON.stringify({ error: "Failed to parse JFR output. Ensure recording used settings=profile." });
|
|
56
|
+
}
|
|
57
|
+
const topAllocators = [...allocatorCount.entries()]
|
|
58
|
+
.sort((a, b) => b[1] - a[1])
|
|
59
|
+
.slice(0, topN)
|
|
60
|
+
.map(([allocator, count]) => ({ allocator, count }));
|
|
61
|
+
const result = {
|
|
62
|
+
topAllocators,
|
|
63
|
+
gcStats: { gcEvents: gcCount },
|
|
64
|
+
potentialLeaks: potentialLeaks.length ? [...new Set(potentialLeaks)].slice(0, 5) : undefined,
|
|
65
|
+
};
|
|
66
|
+
return JSON.stringify(result, null, 2);
|
|
67
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const profileTimeSchema: z.ZodObject<{
|
|
3
|
+
filepath: z.ZodString;
|
|
4
|
+
topN: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
|
|
5
|
+
}, "strip", z.ZodTypeAny, {
|
|
6
|
+
topN: number;
|
|
7
|
+
filepath: string;
|
|
8
|
+
}, {
|
|
9
|
+
filepath: string;
|
|
10
|
+
topN?: number | undefined;
|
|
11
|
+
}>;
|
|
12
|
+
export type ProfileTimeInput = z.infer<typeof profileTimeSchema>;
|
|
13
|
+
export declare function profileTime(input: ProfileTimeInput): Promise<string>;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { runJfr } from "../utils/jdk.js";
|
|
4
|
+
import { resolveProfilePath } from "../utils/paths.js";
|
|
5
|
+
import { getEvents, getStackTrace, getMethodKey } from "../utils/jfr-json.js";
|
|
6
|
+
export const profileTimeSchema = z.object({
|
|
7
|
+
filepath: z.string(),
|
|
8
|
+
topN: z.number().int().min(1).max(100).optional().default(10),
|
|
9
|
+
});
|
|
10
|
+
export async function profileTime(input) {
|
|
11
|
+
const { topN } = input;
|
|
12
|
+
const filepath = resolveProfilePath(input.filepath);
|
|
13
|
+
if (!existsSync(filepath)) {
|
|
14
|
+
return JSON.stringify({ error: `File not found: ${filepath}` });
|
|
15
|
+
}
|
|
16
|
+
const output = await runJfr(["print", "--json", "--events", "jdk.ExecutionSample", filepath]);
|
|
17
|
+
const methodSamples = new Map();
|
|
18
|
+
try {
|
|
19
|
+
const parsed = JSON.parse(output);
|
|
20
|
+
const eventsList = getEvents(parsed);
|
|
21
|
+
for (const ev of eventsList) {
|
|
22
|
+
const frames = getStackTrace(ev)?.frames ?? [];
|
|
23
|
+
for (const f of frames) {
|
|
24
|
+
const key = getMethodKey(f);
|
|
25
|
+
if (key)
|
|
26
|
+
methodSamples.set(key, (methodSamples.get(key) ?? 0) + 1);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return JSON.stringify({ error: "Failed to parse JFR ExecutionSample output." });
|
|
32
|
+
}
|
|
33
|
+
const top = [...methodSamples.entries()]
|
|
34
|
+
.sort((a, b) => b[1] - a[1])
|
|
35
|
+
.slice(0, topN)
|
|
36
|
+
.map(([method, samples]) => ({ method, samples, note: "cumulative CPU time (incl. callees)" }));
|
|
37
|
+
return JSON.stringify({ profile: "time", topMethods: top }, null, 2);
|
|
38
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const startProfilingSchema: z.ZodObject<{
|
|
3
|
+
pid: z.ZodNumber;
|
|
4
|
+
duration: z.ZodNumber;
|
|
5
|
+
}, "strip", z.ZodTypeAny, {
|
|
6
|
+
pid: number;
|
|
7
|
+
duration: number;
|
|
8
|
+
}, {
|
|
9
|
+
pid: number;
|
|
10
|
+
duration: number;
|
|
11
|
+
}>;
|
|
12
|
+
export type StartProfilingInput = z.infer<typeof startProfilingSchema>;
|
|
13
|
+
export declare function startProfiling(input: StartProfilingInput): Promise<string>;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, renameSync, unlinkSync } from "node:fs";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { runJcmd } from "../utils/jdk.js";
|
|
4
|
+
import { NEW_PROFILE_PATH, OLD_PROFILE_PATH, RECORDINGS_DIR, } from "../utils/paths.js";
|
|
5
|
+
export const startProfilingSchema = z.object({
|
|
6
|
+
pid: z.number().int().positive(),
|
|
7
|
+
duration: z.number().int().positive(),
|
|
8
|
+
});
|
|
9
|
+
function rotateProfiles() {
|
|
10
|
+
if (!existsSync(RECORDINGS_DIR)) {
|
|
11
|
+
mkdirSync(RECORDINGS_DIR, { recursive: true });
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
if (existsSync(OLD_PROFILE_PATH)) {
|
|
15
|
+
unlinkSync(OLD_PROFILE_PATH);
|
|
16
|
+
}
|
|
17
|
+
if (existsSync(NEW_PROFILE_PATH)) {
|
|
18
|
+
renameSync(NEW_PROFILE_PATH, OLD_PROFILE_PATH);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export async function startProfiling(input) {
|
|
22
|
+
const { pid, duration } = input;
|
|
23
|
+
rotateProfiles();
|
|
24
|
+
const cmd = `JFR.start duration=${duration}s settings=profile`;
|
|
25
|
+
const output = runJcmd(pid, cmd);
|
|
26
|
+
const match = output.match(/Started recording (\d+)\./);
|
|
27
|
+
const recordingId = match ? match[1] : "1";
|
|
28
|
+
return JSON.stringify({
|
|
29
|
+
recordingId,
|
|
30
|
+
status: "started",
|
|
31
|
+
message: output.trim(),
|
|
32
|
+
expiryTime: `in ${duration} seconds`,
|
|
33
|
+
newProfilePath: NEW_PROFILE_PATH,
|
|
34
|
+
oldProfilePath: OLD_PROFILE_PATH,
|
|
35
|
+
}, null, 2);
|
|
36
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const stopProfilingSchema: z.ZodObject<{
|
|
3
|
+
pid: z.ZodNumber;
|
|
4
|
+
recordingId: z.ZodString;
|
|
5
|
+
}, "strip", z.ZodTypeAny, {
|
|
6
|
+
pid: number;
|
|
7
|
+
recordingId: string;
|
|
8
|
+
}, {
|
|
9
|
+
pid: number;
|
|
10
|
+
recordingId: string;
|
|
11
|
+
}>;
|
|
12
|
+
export type StopProfilingInput = z.infer<typeof stopProfilingSchema>;
|
|
13
|
+
export declare function stopProfiling(input: StopProfilingInput): Promise<string>;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { existsSync, mkdirSync } from "node:fs";
|
|
2
|
+
import { statSync } from "node:fs";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { runJcmd } from "../utils/jdk.js";
|
|
5
|
+
import { NEW_PROFILE_PATH, RECORDINGS_DIR } from "../utils/paths.js";
|
|
6
|
+
export const stopProfilingSchema = z.object({
|
|
7
|
+
pid: z.number().int().positive(),
|
|
8
|
+
recordingId: z.string(),
|
|
9
|
+
});
|
|
10
|
+
export async function stopProfiling(input) {
|
|
11
|
+
const { pid, recordingId } = input;
|
|
12
|
+
if (!existsSync(RECORDINGS_DIR)) {
|
|
13
|
+
mkdirSync(RECORDINGS_DIR, { recursive: true });
|
|
14
|
+
}
|
|
15
|
+
runJcmd(pid, "JFR.stop", [`name=${recordingId}`, `filename=${NEW_PROFILE_PATH}`]);
|
|
16
|
+
if (!existsSync(NEW_PROFILE_PATH)) {
|
|
17
|
+
return JSON.stringify({
|
|
18
|
+
status: "error",
|
|
19
|
+
message: "Recording stopped but file was not found. Check JFR output.",
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
const stats = statSync(NEW_PROFILE_PATH);
|
|
23
|
+
return JSON.stringify({
|
|
24
|
+
filepath: NEW_PROFILE_PATH,
|
|
25
|
+
fileSize: stats.size,
|
|
26
|
+
status: "saved",
|
|
27
|
+
oldProfilePath: "recordings/old_profile.jfr (previous recording)",
|
|
28
|
+
hint: "Use recordings/new_profile.jfr for current, recordings/old_profile.jfr for previous (before/after comparison)",
|
|
29
|
+
}, null, 2);
|
|
30
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const traceMethodSchema: z.ZodObject<{
|
|
3
|
+
filepath: z.ZodString;
|
|
4
|
+
className: z.ZodString;
|
|
5
|
+
methodName: z.ZodString;
|
|
6
|
+
events: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
7
|
+
topN: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
|
|
8
|
+
}, "strip", z.ZodTypeAny, {
|
|
9
|
+
topN: number;
|
|
10
|
+
filepath: string;
|
|
11
|
+
className: string;
|
|
12
|
+
methodName: string;
|
|
13
|
+
events?: string[] | undefined;
|
|
14
|
+
}, {
|
|
15
|
+
filepath: string;
|
|
16
|
+
className: string;
|
|
17
|
+
methodName: string;
|
|
18
|
+
topN?: number | undefined;
|
|
19
|
+
events?: string[] | undefined;
|
|
20
|
+
}>;
|
|
21
|
+
export type TraceMethodInput = z.infer<typeof traceMethodSchema>;
|
|
22
|
+
export declare function traceMethod(input: TraceMethodInput): Promise<string>;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { runJfr } from "../utils/jdk.js";
|
|
4
|
+
import { resolveProfilePath } from "../utils/paths.js";
|
|
5
|
+
import { getEvents, getStackTrace, getMethodKey } from "../utils/jfr-json.js";
|
|
6
|
+
export const traceMethodSchema = z.object({
|
|
7
|
+
filepath: z.string(),
|
|
8
|
+
className: z.string(),
|
|
9
|
+
methodName: z.string(),
|
|
10
|
+
events: z.array(z.string()).optional(),
|
|
11
|
+
topN: z.number().int().min(1).max(100).optional().default(10),
|
|
12
|
+
});
|
|
13
|
+
export async function traceMethod(input) {
|
|
14
|
+
const { className, methodName, topN } = input;
|
|
15
|
+
const filepath = resolveProfilePath(input.filepath);
|
|
16
|
+
if (!existsSync(filepath)) {
|
|
17
|
+
return JSON.stringify({ error: `File not found: ${filepath}` });
|
|
18
|
+
}
|
|
19
|
+
const events = input.events ?? ["jdk.ExecutionSample"];
|
|
20
|
+
const eventsArg = events.join(",");
|
|
21
|
+
const output = await runJfr(["print", "--json", "--events", eventsArg, filepath]);
|
|
22
|
+
let eventsList;
|
|
23
|
+
try {
|
|
24
|
+
const parsed = JSON.parse(output);
|
|
25
|
+
eventsList = getEvents(parsed);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return JSON.stringify({ error: "Failed to parse JFR JSON output" });
|
|
29
|
+
}
|
|
30
|
+
const targetMethod = `${className}.${methodName}`;
|
|
31
|
+
const matchingPaths = new Map();
|
|
32
|
+
const classNorm = className.replace(/\//g, ".");
|
|
33
|
+
for (const ev of eventsList) {
|
|
34
|
+
const frames = getStackTrace(ev)?.frames ?? [];
|
|
35
|
+
const pathParts = [];
|
|
36
|
+
let found = false;
|
|
37
|
+
for (const f of frames) {
|
|
38
|
+
const fullMethod = getMethodKey(f);
|
|
39
|
+
if (fullMethod)
|
|
40
|
+
pathParts.push(fullMethod);
|
|
41
|
+
if (fullMethod.includes(classNorm) && fullMethod.includes(methodName)) {
|
|
42
|
+
found = true;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (found && pathParts.length > 0) {
|
|
46
|
+
const path = pathParts.join(" <- ");
|
|
47
|
+
matchingPaths.set(path, (matchingPaths.get(path) ?? 0) + 1);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (matchingPaths.size === 0) {
|
|
51
|
+
return `Method ${targetMethod} not found in ExecutionSample. Try a longer recording or ensure the method is invoked during profiling.`;
|
|
52
|
+
}
|
|
53
|
+
const sorted = [...matchingPaths.entries()]
|
|
54
|
+
.sort((a, b) => b[1] - a[1])
|
|
55
|
+
.slice(0, topN);
|
|
56
|
+
const lines = sorted.map(([path, count], i) => `${i + 1}. [${count}x] ${path}`);
|
|
57
|
+
return "Call tree (top paths where method appears):\n\n" + lines.join("\n");
|
|
58
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const vmInfoSchema: z.ZodObject<{
|
|
3
|
+
pid: z.ZodNumber;
|
|
4
|
+
}, "strip", z.ZodTypeAny, {
|
|
5
|
+
pid: number;
|
|
6
|
+
}, {
|
|
7
|
+
pid: number;
|
|
8
|
+
}>;
|
|
9
|
+
export type VmInfoInput = z.infer<typeof vmInfoSchema>;
|
|
10
|
+
export declare function vmInfo(input: VmInfoInput): Promise<string>;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { runJcmd } from "../utils/jdk.js";
|
|
3
|
+
export const vmInfoSchema = z.object({
|
|
4
|
+
pid: z.number().int().positive(),
|
|
5
|
+
});
|
|
6
|
+
export async function vmInfo(input) {
|
|
7
|
+
const { pid } = input;
|
|
8
|
+
const [uptimeOut, versionOut, flagsOut] = [
|
|
9
|
+
runJcmd(pid, "VM.uptime"),
|
|
10
|
+
runJcmd(pid, "VM.version"),
|
|
11
|
+
runJcmd(pid, "VM.flags"),
|
|
12
|
+
];
|
|
13
|
+
const result = {
|
|
14
|
+
uptime: uptimeOut.trim(),
|
|
15
|
+
version: versionOut.trim(),
|
|
16
|
+
flags: flagsOut.trim(),
|
|
17
|
+
};
|
|
18
|
+
return JSON.stringify(result, null, 2);
|
|
19
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface JavaProcess {
|
|
2
|
+
pid: number;
|
|
3
|
+
mainClass: string;
|
|
4
|
+
args: string;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Run jps -l -m and parse output into structured process list.
|
|
8
|
+
*/
|
|
9
|
+
export declare function runJps(): JavaProcess[];
|
|
10
|
+
/**
|
|
11
|
+
* Run jcmd with given pid and command.
|
|
12
|
+
*/
|
|
13
|
+
export declare function runJcmd(pid: number, command: string, options?: string[]): string;
|
|
14
|
+
/**
|
|
15
|
+
* Run jfr command (print, summary, etc.) and return stdout.
|
|
16
|
+
*/
|
|
17
|
+
export declare function runJfr(args: string[]): Promise<string>;
|