Skip to content

Commit 5416d4b

Browse files
authored
Add LS daemon to monitor error and performance (#925)
1 parent b0acc90 commit 5416d4b

File tree

10 files changed

+499
-9
lines changed

10 files changed

+499
-9
lines changed

package-lock.json

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
"publisher": "vscjava",
88
"preview": true,
99
"engines": {
10-
"vscode": "^1.52.0"
10+
"vscode": "^1.64.0"
1111
},
1212
"aiKey": "b4a8a622-6ac7-4cf8-83aa-f325e1890795",
1313
"icon": "logo.png",
@@ -218,6 +218,12 @@
218218
"default": true,
219219
"description": "Show release notes of Extension Pack for Java on startup.",
220220
"scope": "window"
221+
},
222+
"java.help.shareDiagnostics": {
223+
"type": "boolean",
224+
"default": false,
225+
"description": "Whether to send back detailed information from error logs for diagnostic purpose.",
226+
"scope": "window"
221227
}
222228
}
223229
},
@@ -295,7 +301,7 @@
295301
"@types/react-dom": "^16.9.14",
296302
"@types/react-redux": "^7.1.22",
297303
"@types/semver": "^5.5.0",
298-
"@types/vscode": "1.52.0",
304+
"@types/vscode": "1.64.0",
299305
"@types/winreg": "^1.2.31",
300306
"@types/xmldom": "^0.1.31",
301307
"autoprefixer": "^10.4.2",

src/daemon/daemon.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT license.
3+
4+
import * as vscode from "vscode";
5+
import { ProcessWatcher } from "./processWatcher";
6+
import { LogWatcher } from "./serverLog/logWatcher";
7+
8+
9+
export class LSDaemon {
10+
11+
public logWatcher: LogWatcher;
12+
public processWatcher: ProcessWatcher;
13+
14+
constructor(public context: vscode.ExtensionContext) {
15+
this.processWatcher = new ProcessWatcher(this);
16+
this.logWatcher = new LogWatcher(this);
17+
}
18+
19+
public async initialize() {
20+
await this.logWatcher.start();
21+
}
22+
23+
24+
}

src/daemon/index.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT license.
3+
4+
import { promisify } from "util";
5+
import * as vscode from "vscode";
6+
import { sendError } from "vscode-extension-telemetry-wrapper";
7+
import { LSDaemon } from "./daemon";
8+
9+
const delay = promisify(setTimeout);
10+
11+
let daemon: LSDaemon;
12+
export async function initDaemon(context: vscode.ExtensionContext) {
13+
daemon = new LSDaemon(context);
14+
await daemon.initialize()
15+
16+
const activated = await checkJavaExtActivated(context);
17+
if (activated) {
18+
daemon.logWatcher.sendStartupMetadata("redhat.java activated");
19+
}
20+
}
21+
22+
async function checkJavaExtActivated(_context: vscode.ExtensionContext): Promise<boolean> {
23+
const javaExt = vscode.extensions.getExtension("redhat.java");
24+
if (!javaExt) {
25+
return false;
26+
}
27+
28+
// wait javaExt to activate
29+
const timeout = 30 * 60 * 1000; // wait 30 min at most
30+
let count = 0;
31+
while(!javaExt.isActive && count < timeout) {
32+
await delay(1000);
33+
count += 1000;
34+
}
35+
36+
if (!javaExt.isActive) {
37+
sendError(new Error("redhat.java extension not activated within 30 min"));
38+
daemon.logWatcher.sendStartupMetadata("redhat.java activation timeout");
39+
return false;
40+
}
41+
42+
// on ServiceReady
43+
javaExt.exports.onDidServerModeChange(async (mode: string) => {
44+
if (mode === "Standard") {
45+
daemon.logWatcher.sendStartupMetadata("jdtls standard server ready");
46+
47+
// watchdog
48+
if (await daemon.processWatcher.start()) {
49+
daemon.processWatcher.monitor();
50+
} else {
51+
sendError(new Error("jdtls watchdog is not started"));
52+
}
53+
}
54+
});
55+
56+
return true;
57+
}

src/daemon/processWatcher.ts

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT license.
3+
4+
import * as cp from "child_process";
5+
import * as fs from "fs";
6+
import * as os from "os";
7+
import * as path from "path";
8+
import { promisify } from "util";
9+
import * as vscode from "vscode";
10+
import { sendInfo } from "vscode-extension-telemetry-wrapper";
11+
import { LSDaemon } from "./daemon";
12+
const execFile = promisify(cp.execFile);
13+
14+
interface IJdtlsMetadata {
15+
pid?: string;
16+
jreHome?: string;
17+
workspace?: string;
18+
}
19+
20+
21+
export class ProcessWatcher {
22+
private workspace?: string;
23+
private pid?: string;
24+
private jreHome?: string;
25+
private lastHeartbeat?: string;
26+
private context: vscode.ExtensionContext
27+
constructor(private daemon: LSDaemon) {
28+
this.context = daemon.context;
29+
}
30+
31+
public async start(): Promise<boolean> {
32+
const javaExt = vscode.extensions.getExtension("redhat.java");
33+
if (!javaExt) {
34+
return false;
35+
}
36+
// get embedded JRE Home
37+
let jreHome: string | undefined;
38+
try {
39+
const jreFolder = path.join(javaExt.extensionPath, "jre");
40+
const jreDistros = await fs.promises.readdir(jreFolder);
41+
if (jreDistros.length > 0) {
42+
jreHome = path.join(jreFolder, jreDistros[0]);
43+
}
44+
} catch (error) {
45+
console.error(error);
46+
}
47+
if (!jreHome) {
48+
return false;
49+
}
50+
this.jreHome = jreHome;
51+
52+
const jdtlsmeta = await getPidAndWS(jreHome, this.context);
53+
this.pid = jdtlsmeta.pid;
54+
this.workspace = jdtlsmeta.workspace;
55+
56+
return (this.pid !== undefined && this.workspace !== undefined);
57+
}
58+
59+
public monitor() {
60+
const id = setInterval(() => {
61+
this.upTime().then(seconds => {
62+
this.lastHeartbeat = seconds;
63+
}).catch(_e => {
64+
clearInterval(id);
65+
this.onDidJdtlsCrash(this.lastHeartbeat);
66+
});
67+
}, 5000);
68+
// TBD: e.g. constantly monitor heap size and uptime
69+
}
70+
71+
public async upTime(): Promise<string | undefined> {
72+
if (!this.jreHome || !this.pid) {
73+
throw new Error("unsupported");
74+
}
75+
76+
const execRes = await execFile(path.join(this.jreHome, "bin", "jcmd"), [this.pid, "VM.uptime"]);
77+
const r = /\d+\.\d+ s/;
78+
return execRes.stdout.match(r)?.toString();
79+
}
80+
81+
public async heapSize(): Promise<string> {
82+
if (!this.jreHome || !this.pid) {
83+
throw new Error("unsupported");
84+
}
85+
86+
const execRes = await execFile(path.join(this.jreHome, "bin", "jcmd"), [this.pid, "GC.heap_info"]);
87+
const ryoung = /PSYoungGen\s+total \d+K, used \d+K/;
88+
const y = execRes.stdout.match(ryoung)?.toString();
89+
90+
const rold = /ParOldGen\s+total \d+K, used \d+K/;
91+
const o = execRes.stdout.match(rold)?.toString();
92+
return [y, o].join(os.EOL);
93+
}
94+
95+
private onDidJdtlsCrash(lastHeartbeat?: string) {
96+
sendInfo("", {
97+
name: "jdtls-last-heartbeat",
98+
message: lastHeartbeat!
99+
});
100+
const consentToCollectLogs = vscode.workspace.getConfiguration("java").get<boolean>("help.shareDiagnostics");
101+
if (!consentToCollectLogs) {
102+
vscode.window.showInformationMessage("Java Language Server is crashed. Do you want to share error logs with us for diagnostic purpose?", "Yes").then(choice => {
103+
if (choice) {
104+
vscode.workspace.getConfiguration("java").update("help.shareDiagnostics", true);
105+
this.daemon.logWatcher.sendErrorAndStackOnCrash();
106+
}
107+
});
108+
}
109+
}
110+
}
111+
112+
113+
function parseJdtlsJps(jdtlsJpsLine: string): IJdtlsMetadata {
114+
const spaceIdx = jdtlsJpsLine.indexOf(" ");
115+
const pid = jdtlsJpsLine.slice(0, spaceIdx);
116+
const cmd = jdtlsJpsLine.slice(spaceIdx + 1);
117+
const res = cmd.match(/-XX:HeapDumpPath=(.*(redhat.java|vscodesws_[0-9a-f]{5}))/);
118+
let workspace;
119+
if (res && res[1]) {
120+
workspace = res[1];
121+
}
122+
123+
return {
124+
pid,
125+
workspace
126+
};
127+
}
128+
129+
130+
async function getPidAndWS(jreHome: string, context:vscode.ExtensionContext) {
131+
const jpsExecRes = await execFile(path.join(jreHome, "bin", "jps"), ["-v"]);
132+
const jdtlsLines = jpsExecRes.stdout.split(os.EOL).filter(line => line.includes("org.eclipse.jdt.ls.core"));
133+
let jdtlsJpsLine;
134+
if (context.storageUri) {
135+
jdtlsJpsLine = jdtlsLines.find(line => line.includes(path.dirname(context.storageUri!.fsPath)));
136+
} else {
137+
jdtlsJpsLine = jdtlsLines.find(line => line.includes("vscodesws_"));
138+
}
139+
140+
return jdtlsJpsLine ? parseJdtlsJps(jdtlsJpsLine) : {};
141+
}

0 commit comments

Comments
 (0)