Skip to content

Commit 96e1890

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 96e1890

File tree

9 files changed

+185
-29
lines changed

9 files changed

+185
-29
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 `-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: 31 additions & 15 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: 68 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,29 @@
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 glob = require("glob");
37

48
/**
59
* An error handler that restarts the language server,
6-
* unless it has been restarted 5 times in the last 3 minutes
10+
* unless it has been restarted 5 times in the last 3 minutes,
11+
* or if it crashed due to an Out Of Memory Error
712
*
813
* Adapted from [vscode-java](https://github.com/redhat-developer/vscode-java)
914
*/
1015
export class ClientErrorHandler implements ErrorHandler {
1116

1217
private restarts: number[];
1318
private name: string;
19+
private context: ExtensionContext;
20+
private heapDumpFolder: string;
1421

15-
constructor(name: string) {
22+
constructor(name: string, context: ExtensionContext) {
1623
this.name = name;
1724
this.restarts = [];
25+
this.context = context;
26+
this.heapDumpFolder = getHeapDumpFolderFromSettings() || context.globalStorageUri.path;
1827
}
1928

2029
error(_error: Error, _message: Message, _count: number): ErrorAction {
@@ -23,6 +32,17 @@ export class ClientErrorHandler implements ErrorHandler {
2332

2433
closed(): CloseAction {
2534
this.restarts.push(Date.now());
35+
const heapProfileGlob = new glob.GlobSync(`${this.heapDumpFolder}/java_*.hprof`);
36+
if (heapProfileGlob.found.length) {
37+
// Only clean heap dumps that are generated in the default location.
38+
// The default location is the extension global storage
39+
// This means that if users change the folder where the heap dumps are placed,
40+
// then they will be able to read the heap dumps,
41+
// since they aren't immediately deleted.
42+
cleanUpHeapDumps(this.context);
43+
showOOMMessage();
44+
return CloseAction.DoNotRestart;
45+
}
2646
if (this.restarts.length < 5) {
2747
return CloseAction.Restart;
2848
} else {
@@ -37,3 +57,48 @@ export class ClientErrorHandler implements ErrorHandler {
3757
}
3858

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

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: 11 additions & 2 deletions
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';
15-
import { ExtensionContext, extensions, languages } from "vscode";
16+
import { ExtensionContext, extensions, languages, window } 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,13 @@ export async function activate(context: ExtensionContext): Promise<XMLExtensionA
5254
storagePath = os.homedir() + "/.lemminx";
5355
}
5456
const logfile = path.resolve(storagePath + '/lemminx.log');
57+
let xmlStorage = context.globalStorageUri.path;
58+
if (os.platform() === "win32") {
59+
xmlStorage = xmlStorage.substring(1);
60+
}
61+
window.showInformationMessage(xmlStorage);
62+
await fs.ensureDir(xmlStorage);
63+
await cleanUpHeapDumps(context);
5564

5665
const externalXmlSettings: ExternalXmlSettings = new ExternalXmlSettings();
5766

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)