Skip to content

Commit 064ac73

Browse files
committed
Handle OOM in Java language server
Add parameters to the java server to make lemminx crash when it runs out of memory. Detect when the java server shuts down due to running out of memory, display a message to the user that this happened, and don't attempt to restart the server. Closes #527 Signed-off-by: David Thompson <davthomp@redhat.com>
1 parent f164222 commit 064ac73

File tree

11 files changed

+204
-29
lines changed

11 files changed

+204
-29
lines changed

USAGE_DATA.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ vscode-xml has opt-in telemetry collection, provided by [vscode-redhat-telemetry
1818
* If the download fails, the associated error is attached to the telemetry event
1919
* A telemetry event is sent every time you click the "Open Proxy Configuration Documentation" link that is provided when the language server binary download fails due to a proxy related issue.
2020
* A telemetry event is sent every time you click the "Download Java" link that appears when you have [LemMinX extensions](./docs/Extensions.md) installed but don't have Java installed.
21+
* A telemetry event is sent every time the Java XML language server crashes due to an Out Of Memory Error, which also contains the maximum heap space for the JVM (-Xmx) that you've set.
22+
* A telemetry event is sent every time you click on the link to the documentation that appears after the Java XML language server crashes due to an Out Of Memory Error.
2123

2224
## What's included in the general telemetry data
2325

docs/Troubleshooting.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,22 @@ You can kill the process by:
2121

2222
* on Windows OS: run `taskkill /F /PID ...` all instances
2323
* on other OS: run `kill -9 ...` all instances
24+
25+
### The Language Server Crashes Due to an Out Of Memory Error
26+
27+
If you are working with large XML files or referencing large schema files,
28+
this may lead to the language server running out of memory.
29+
The Java language server is more likely to run out memory than the binary language server.
30+
Switching to the binary language server
31+
or increasing the memory available to the Java language server could resolve this issue.
32+
33+
If you get an Out of Memory Error, but aren't working with large XML files,
34+
then there may be a memory leak in the language server.
35+
Please [file a issue](https://github.com/redhat-developer/vscode-xml/issues) with a description of what you were doing if this is the case.
36+
37+
#### How to increase the amount of memory available to the Java Language Server
38+
39+
1. Go to settings
40+
2. Navigate to the setting `xml.server.vmargs`
41+
3. Add `-Xmx512m` to the setting string. This allows the the language server to use at most 512 megabytes of memory.
42+
4. If the problem persists, you can increase the `512m` to `1G` or higher

package-lock.json

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

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,8 @@
6767
"Snippets"
6868
],
6969
"devDependencies": {
70-
"@types/fs-extra": "^8.0.0",
70+
"@types/fs-extra": "^8.1.2",
71+
"@types/glob": "^7.1.4",
7172
"@types/node": "^10.14.16",
7273
"@types/vscode": "^1.37.0",
7374
"@types/yauzl": "^2.9.1",

src/client/clientErrorHandler.ts

Lines changed: 91 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,30 @@
1-
import { window } from "vscode";
1+
import * as fs from "fs-extra";
2+
import { commands, ExtensionContext, window, workspace } from "vscode";
23
import { CloseAction, ErrorAction, ErrorHandler, Message } from "vscode-languageclient";
4+
import { ClientCommandConstants } from "../commands/commandConstants";
5+
import { HEAP_DUMP_LOCATION } from "../server/java/jvmArguments";
6+
import { Telemetry } from "../telemetry";
7+
import glob = require("glob");
38

49
/**
510
* An error handler that restarts the language server,
6-
* unless it has been restarted 5 times in the last 3 minutes
11+
* unless it has been restarted 5 times in the last 10 minutes,
12+
* or if it crashed due to an Out Of Memory Error
713
*
814
* Adapted from [vscode-java](https://github.com/redhat-developer/vscode-java)
915
*/
1016
export class ClientErrorHandler implements ErrorHandler {
1117

1218
private restarts: number[];
1319
private name: string;
20+
private context: ExtensionContext;
21+
private heapDumpFolder: string;
1422

15-
constructor(name: string) {
23+
constructor(name: string, context: ExtensionContext) {
1624
this.name = name;
1725
this.restarts = [];
26+
this.context = context;
27+
this.heapDumpFolder = getHeapDumpFolderFromSettings() || context.globalStorageUri.fsPath;
1828
}
1929

2030
error(_error: Error, _message: Message, _count: number): ErrorAction {
@@ -23,12 +33,24 @@ export class ClientErrorHandler implements ErrorHandler {
2333

2434
closed(): CloseAction {
2535
this.restarts.push(Date.now());
36+
const heapProfileGlob = new glob.GlobSync(`${this.heapDumpFolder}/java_*.hprof`);
37+
if (heapProfileGlob.found.length) {
38+
// Only clean heap dumps that are generated in the default location.
39+
// The default location is the extension global storage
40+
// This means that if users change the folder where the heap dumps are placed,
41+
// then they will be able to read the heap dumps,
42+
// since they aren't immediately deleted.
43+
cleanUpHeapDumps(this.context);
44+
Telemetry.sendTelemetry(Telemetry.JAVA_OOM_EVT, { 'jvm.xmx': getXmxFromSettings() });
45+
showOOMMessage();
46+
return CloseAction.DoNotRestart;
47+
}
2648
if (this.restarts.length < 5) {
2749
return CloseAction.Restart;
2850
} else {
2951
const diff = this.restarts[this.restarts.length - 1] - this.restarts[0];
30-
if (diff <= 3 * 60 * 1000) {
31-
window.showErrorMessage(`The ${this.name} language server crashed 5 times in the last 3 minutes. The server will not be restarted.`);
52+
if (diff <= 10 * 60 * 1000) {
53+
window.showErrorMessage(`The ${this.name} language server crashed 5 times in the last 10 minutes. The server will not be restarted.`);
3254
return CloseAction.DoNotRestart;
3355
}
3456
this.restarts.shift();
@@ -37,3 +59,67 @@ export class ClientErrorHandler implements ErrorHandler {
3759
}
3860

3961
}
62+
63+
/**
64+
* Deletes all the heap dumps generated by Out Of Memory errors
65+
*
66+
* @returns when the heap dumps have been deleted
67+
*/
68+
export async function cleanUpHeapDumps(context: ExtensionContext): Promise<void> {
69+
const heapProfileGlob = new glob.GlobSync(`${context.globalStorageUri.fsPath}/java_*.hprof`);
70+
for (let heapProfile of heapProfileGlob.found) {
71+
await fs.remove(heapProfile);
72+
}
73+
}
74+
75+
/**
76+
* Shows a message about the server crashing due to an out of memory issue
77+
*/
78+
async function showOOMMessage(): Promise<void> {
79+
const DOCS = 'More info...';
80+
const result = await window.showErrorMessage('The XML Language Server crashed due to an Out Of Memory Error, and will not be restarted. ', //
81+
DOCS);
82+
if (result === DOCS) {
83+
Telemetry.sendTelemetry(Telemetry.OPEN_OOM_DOCS_EVT);
84+
await commands.executeCommand(ClientCommandConstants.OPEN_DOCS,
85+
{
86+
page: 'Troubleshooting',
87+
section: 'the-language-server-crashes-due-to-an-out-of-memory-error'
88+
}
89+
);
90+
}
91+
}
92+
93+
const HEAP_DUMP_FOLDER_EXTRACTOR = new RegExp(`${HEAP_DUMP_LOCATION}(?:'([^']+)'|"([^"]+)"|([^\\s]+))`);
94+
95+
/**
96+
* Returns the heap dump folder defined in the user's preferences, or undefined if the user does not set the heap dump folder
97+
*
98+
* @returns the heap dump folder defined in the user's preferences, or undefined if the user does not set the heap dump folder
99+
*/
100+
function getHeapDumpFolderFromSettings(): string {
101+
const jvmArgs: string = workspace.getConfiguration('xml.server').get('vmargs');
102+
const results = HEAP_DUMP_FOLDER_EXTRACTOR.exec(jvmArgs);
103+
if (!results || !results[0]) {
104+
return undefined;
105+
}
106+
return results[1] || results[2] || results[3];
107+
}
108+
109+
const XMX_EXTRACTOR = /-Xmx([^\s]+)/;
110+
111+
/**
112+
* Returns the value that the user set for Xmx, or DEFAULT if the user didn't set Xmx
113+
*
114+
* @returns the value that the user set for Xmx, or DEFAULT if the user didn't set Xmx
115+
*/
116+
function getXmxFromSettings(): string {
117+
const vmargs: string = workspace.getConfiguration('xml.server').get('vmargs', null);
118+
if (vmargs != null) {
119+
const extractOfVmargs: RegExpExecArray = XMX_EXTRACTOR.exec(vmargs);
120+
if (extractOfVmargs.length && extractOfVmargs[1]) {
121+
return extractOfVmargs[1];
122+
}
123+
}
124+
return 'DEFAULT';
125+
}

src/client/xmlClient.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { commands, ExtensionContext, extensions, Position, TextDocument, TextEdi
33
import { Command, ConfigurationParams, ConfigurationRequest, DidChangeConfigurationNotification, ExecuteCommandParams, LanguageClientOptions, MessageType, NotificationType, RequestType, RevealOutputChannelOn, TextDocumentPositionParams } from "vscode-languageclient";
44
import { Executable, LanguageClient } from 'vscode-languageclient/node';
55
import { XMLFileAssociation } from '../api/xmlExtensionApi';
6-
import { ClientCommandConstants, ServerCommandConstants } from '../commands/commandConstants';
6+
import { ServerCommandConstants } from '../commands/commandConstants';
77
import { registerClientServerCommands } from '../commands/registerCommands';
88
import { onExtensionChange } from '../plugin';
99
import { RequirementsData } from "../server/requirements";
@@ -37,7 +37,7 @@ let languageClient: LanguageClient;
3737

3838
export async function startLanguageClient(context: ExtensionContext, executable: Executable, logfile: string, externalXmlSettings: ExternalXmlSettings, requirementsData: RequirementsData): Promise<LanguageClient> {
3939

40-
const languageClientOptions: LanguageClientOptions = getLanguageClientOptions(logfile, externalXmlSettings, requirementsData);
40+
const languageClientOptions: LanguageClientOptions = getLanguageClientOptions(logfile, externalXmlSettings, requirementsData, context);
4141
languageClient = new LanguageClient('xml', 'XML Support', executable, languageClientOptions);
4242

4343
languageClient.onTelemetry(async (e: TelemetryEvent) => {
@@ -106,7 +106,11 @@ export async function startLanguageClient(context: ExtensionContext, executable:
106106
return languageClient;
107107
}
108108

109-
function getLanguageClientOptions(logfile: string, externalXmlSettings: ExternalXmlSettings, requirementsData: RequirementsData): LanguageClientOptions {
109+
function getLanguageClientOptions(
110+
logfile: string,
111+
externalXmlSettings: ExternalXmlSettings,
112+
requirementsData: RequirementsData,
113+
context: ExtensionContext): LanguageClientOptions {
110114
return {
111115
// Register the server for xml and xsl
112116
documentSelector: [
@@ -135,7 +139,7 @@ function getLanguageClientOptions(logfile: string, externalXmlSettings: External
135139
shouldLanguageServerExitOnShutdown: true
136140
}
137141
},
138-
errorHandler: new ClientErrorHandler('XML'),
142+
errorHandler: new ClientErrorHandler('XML', context),
139143
synchronize: {
140144
//preferences starting with these will trigger didChangeConfiguration
141145
configurationSection: ['xml', '[xml]', 'files.trimFinalNewlines', 'files.trimTrailingWhitespace', 'files.insertFinalNewline']

src/extension.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,21 +10,23 @@
1010
* Microsoft Corporation - Auto Closing Tags
1111
*/
1212

13+
import * as fs from 'fs-extra';
1314
import * as os from 'os';
1415
import * as path from 'path';
1516
import { ExtensionContext, extensions, languages } from "vscode";
1617
import { Executable, LanguageClient } from 'vscode-languageclient/node';
1718
import { XMLExtensionApi } from './api/xmlExtensionApi';
1819
import { getXmlExtensionApiImplementation } from './api/xmlExtensionApiImplementation';
20+
import { cleanUpHeapDumps } from './client/clientErrorHandler';
1921
import { getIndentationRules } from './client/indentation';
2022
import { startLanguageClient } from './client/xmlClient';
23+
import { registerClientOnlyCommands } from './commands/registerCommands';
2124
import { collectXmlJavaExtensions } from './plugin';
2225
import * as requirements from './server/requirements';
2326
import { prepareExecutable } from './server/serverStarter';
2427
import { ExternalXmlSettings } from "./settings/externalXmlSettings";
2528
import { getXMLConfiguration } from './settings/settings';
2629
import { Telemetry } from './telemetry';
27-
import { registerClientOnlyCommands } from './commands/registerCommands';
2830

2931
let languageClient: LanguageClient;
3032

@@ -52,6 +54,8 @@ export async function activate(context: ExtensionContext): Promise<XMLExtensionA
5254
storagePath = os.homedir() + "/.lemminx";
5355
}
5456
const logfile = path.resolve(storagePath + '/lemminx.log');
57+
await fs.ensureDir(context.globalStorageUri.fsPath);
58+
await cleanUpHeapDumps(context);
5559

5660
const externalXmlSettings: ExternalXmlSettings = new ExternalXmlSettings();
5761

src/server/binary/binaryServerStarter.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { getProxySettings, getProxySettingsAsEnvironmentVariables, ProxySettings
1313
import { getXMLConfiguration } from "../../settings/settings";
1414
import { Telemetry } from '../../telemetry';
1515
import { addTrustedHash, getTrustedHashes } from './binaryHashManager';
16-
const glob = require('glob');
16+
import glob = require('glob');
1717

1818
const HTTPS_PATTERN = /^https:\/\//;
1919
const JAR_ZIP_AND_HASH_REJECTOR = /(?:\.jar)|(?:\.zip)|(?:\.sha256)$/;
@@ -430,4 +430,4 @@ async function acceptBinaryDownloadResponse(response: http.IncomingMessage): Pro
430430

431431
async function openProxyDocumentation(): Promise<void> {
432432
await commands.executeCommand(ClientCommandConstants.OPEN_DOCS, { page: "Proxy" });
433-
}
433+
}

0 commit comments

Comments
 (0)