Skip to content

Commit fa7d432

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 475372e commit fa7d432

File tree

8 files changed

+125
-13
lines changed

8 files changed

+125
-13
lines changed

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 `-Xmx 1.5G` to the setting string. This allows the the language server to use at most 1.5 gigabytes of memory.
42+
4. If the problem persists, you can increase the `1.5G` to `2G` or higher

package-lock.json

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

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@
6868
],
6969
"devDependencies": {
7070
"@types/fs-extra": "^8.0.0",
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: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,26 @@
1-
import { window } from "vscode";
1+
import * as fs from "fs-extra";
2+
import { commands, ExtensionContext, window } from "vscode";
23
import { CloseAction, ErrorAction, ErrorHandler, Message } from "vscode-languageclient";
4+
import { ClientCommandConstants } from "../commands/commandConstants";
5+
import glob = require("glob");
36

47
/**
58
* An error handler that restarts the language server,
6-
* unless it has been restarted 5 times in the last 3 minutes
9+
* unless it has been restarted 5 times in the last 3 minutes,
10+
* or if it crashed due to an Out Of Memory Error
711
*
812
* Adapted from [vscode-java](https://github.com/redhat-developer/vscode-java)
913
*/
1014
export class ClientErrorHandler implements ErrorHandler {
1115

1216
private restarts: number[];
1317
private name: string;
18+
private context: ExtensionContext;
1419

15-
constructor(name: string) {
20+
constructor(name: string, context: ExtensionContext) {
1621
this.name = name;
1722
this.restarts = [];
23+
this.context = context;
1824
}
1925

2026
error(_error: Error, _message: Message, _count: number): ErrorAction {
@@ -23,6 +29,12 @@ export class ClientErrorHandler implements ErrorHandler {
2329

2430
closed(): CloseAction {
2531
this.restarts.push(Date.now());
32+
const heapProfileGlob = new glob.GlobSync(`${this.context.globalStorageUri.path}/java_*.hprof`);
33+
if (heapProfileGlob.found.length) {
34+
cleanUpHeapDumps(this.context);
35+
showOOMMessage();
36+
return CloseAction.DoNotRestart;
37+
}
2638
if (this.restarts.length < 5) {
2739
return CloseAction.Restart;
2840
} else {
@@ -37,3 +49,32 @@ export class ClientErrorHandler implements ErrorHandler {
3749
}
3850

3951
}
52+
53+
/**
54+
* Deletes all the heap dumps generated by Out Of Memory errors
55+
*
56+
* @returns when the heap dumps have been deleted
57+
*/
58+
export async function cleanUpHeapDumps(context: ExtensionContext): Promise<void> {
59+
const heapProfileGlob = new glob.GlobSync(`${context.globalStorageUri.path}/java_*.hprof`);
60+
for (let heapProfile of heapProfileGlob.found) {
61+
await fs.remove(heapProfile);
62+
}
63+
}
64+
65+
/**
66+
* Shows a message about the server crashing due to an out of memory issue
67+
*/
68+
async function showOOMMessage(): Promise<void> {
69+
const DOCS = 'More info...';
70+
const result = await window.showErrorMessage('The XML Language Server crashed due to an Out Of Memory Error, and will not be restarted. ', //
71+
DOCS);
72+
if (result === DOCS) {
73+
await commands.executeCommand(ClientCommandConstants.OPEN_DOCS,
74+
{
75+
page: 'Troubleshooting',
76+
section: 'the-language-server-crashes-due-to-an-out-of-memory-error'
77+
}
78+
);
79+
}
80+
}

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: [
@@ -133,7 +137,7 @@ function getLanguageClientOptions(logfile: string, externalXmlSettings: External
133137
shouldLanguageServerExitOnShutdown: true
134138
}
135139
},
136-
errorHandler: new ClientErrorHandler('XML'),
140+
errorHandler: new ClientErrorHandler('XML', context),
137141
synchronize: {
138142
//preferences starting with these will trigger didChangeConfiguration
139143
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.path);
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+
}

src/server/java/javaServerStarter.ts

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,31 @@
11
import * as os from 'os';
22
import * as path from 'path';
3-
import { ExtensionContext, workspace } from 'vscode';
3+
import { ExtensionContext, window, workspace } from 'vscode';
44
import { Executable } from 'vscode-languageclient/node';
55
import { getProxySettings, getProxySettingsAsJVMArgs, jvmArgsContainsProxySettings, ProxySettings } from '../../settings/proxySettings';
66
import { getJavaagentFlag, getKey, getXMLConfiguration, IS_WORKSPACE_VMARGS_XML_ALLOWED, xmlServerVmargs } from '../../settings/settings';
77
import { RequirementsData } from '../requirements';
8-
const glob = require('glob');
8+
import glob = require('glob');
99

1010
declare var v8debug;
1111

1212
const DEBUG = (typeof v8debug === 'object') || startedInDebugMode();
1313

14+
/**
15+
* Argument that tells the program to crash when an OutOfMemoryError is raised
16+
*/
17+
const CRASH_ON_OOM = '-XX:+ExitOnOutOfMemoryError';
18+
19+
/**
20+
* Argument that tells the program to generate a heap dump file when an OutOfMemoryError is raised
21+
*/
22+
const HEAP_DUMP = '-XX:+HeapDumpOnOutOfMemoryError';
23+
24+
/**
25+
* Argument that tells the program where to generate the heap dump that is created when an OutOfMemoryError is raised and `HEAP_DUMP` has been passed
26+
*/
27+
const HEAP_DUMP_LOCATION = '-XX:HeapDumpPath=';
28+
1429
export async function prepareJavaExecutable(
1530
context: ExtensionContext,
1631
requirements: RequirementsData,
@@ -63,6 +78,18 @@ function prepareParams(requirements: RequirementsData, xmlJavaExtensions: string
6378
params.push(watchParentProcess + 'false');
6479
}
6580
}
81+
if (vmargs.indexOf(CRASH_ON_OOM) < 0) {
82+
params.push(CRASH_ON_OOM);
83+
}
84+
if (vmargs.indexOf(HEAP_DUMP) < 0) {
85+
params.push(HEAP_DUMP);
86+
}
87+
if (vmargs.indexOf(HEAP_DUMP_LOCATION) < 0) {
88+
params.push(`${HEAP_DUMP_LOCATION}${context.globalStorageUri.path}`);
89+
} else {
90+
window.showWarningMessage('Heap dump location has been modified; if you are experiencing Out Of Memory crashes, vscode-xml won\'t be able to detect them');
91+
}
92+
6693
// "OpenJDK 64-Bit Server VM warning: Options -Xverify:none and -noverify
6794
// were deprecated in JDK 13 and will likely be removed in a future release."
6895
// so only add -noverify for older versions

0 commit comments

Comments
 (0)