Skip to content

Commit 570f095

Browse files
authored
Merge pull request github#2998 from asger-semmle/js/typescript-memory
Approved by erik-krogh
2 parents 7d0911d + 6c1f98a commit 570f095

File tree

2 files changed

+80
-38
lines changed

2 files changed

+80
-38
lines changed

javascript/extractor/lib/typescript/src/main.ts

Lines changed: 29 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,8 @@ class State {
8686
}
8787
let state = new State();
8888

89+
const reloadMemoryThresholdMb = getEnvironmentVariable("SEMMLE_TYPESCRIPT_MEMORY_THRESHOLD", Number, 1000);
90+
8991
/**
9092
* Debugging method for finding cycles in the TypeScript AST. Should not be used in production.
9193
*
@@ -161,6 +163,7 @@ function extractFile(filename: string): string {
161163
function prepareNextFile() {
162164
if (state.pendingResponse != null) return;
163165
if (state.pendingFileIndex < state.pendingFiles.length) {
166+
checkMemoryUsage();
164167
let nextFilename = state.pendingFiles[state.pendingFileIndex];
165168
state.pendingResponse = extractFile(nextFilename);
166169
}
@@ -529,26 +532,40 @@ function getEnvironmentVariable<T>(name: string, parse: (x: string) => T, defaul
529532
return value != null ? parse(value) : defaultValue;
530533
}
531534

535+
/**
536+
* Whether the memory usage was last observed to be above the threshold for restarting the TypeScript compiler.
537+
*
538+
* This is to prevent repeatedly restarting the compiler if the GC does not immediately bring us below the
539+
* threshold again.
540+
*/
541+
let hasReloadedSinceExceedingThreshold = false;
542+
543+
/**
544+
* If memory usage has moved above a the threshold, reboot the TypeScript compiler instance.
545+
*
546+
* Make sure to call this only when stdout has been flushed.
547+
*/
548+
function checkMemoryUsage() {
549+
let bytesUsed = process.memoryUsage().heapUsed;
550+
let megabytesUsed = bytesUsed / 1000000;
551+
if (!hasReloadedSinceExceedingThreshold && megabytesUsed > reloadMemoryThresholdMb && state.project != null) {
552+
console.warn('Restarting TypeScript compiler due to memory usage');
553+
state.project.reload();
554+
hasReloadedSinceExceedingThreshold = true;
555+
}
556+
else if (hasReloadedSinceExceedingThreshold && megabytesUsed < reloadMemoryThresholdMb) {
557+
hasReloadedSinceExceedingThreshold = false;
558+
}
559+
}
560+
532561
function runReadLineInterface() {
533562
reset();
534-
let reloadMemoryThresholdMb = getEnvironmentVariable("SEMMLE_TYPESCRIPT_MEMORY_THRESHOLD", Number, 1000);
535-
let isAboveReloadThreshold = false;
536563
let rl = readline.createInterface({ input: process.stdin, output: process.stdout });
537564
rl.on("line", (line: string) => {
538565
let req: Command = JSON.parse(line);
539566
switch (req.command) {
540567
case "parse":
541568
handleParseCommand(req);
542-
// If memory usage has moved above the threshold, reboot the TypeScript compiler instance.
543-
let bytesUsed = process.memoryUsage().heapUsed;
544-
let megabytesUsed = bytesUsed / 1000000;
545-
if (!isAboveReloadThreshold && megabytesUsed > reloadMemoryThresholdMb && state.project != null) {
546-
console.warn('Restarting TypeScript compiler due to memory usage');
547-
state.project.reload();
548-
isAboveReloadThreshold = true;
549-
} else if (isAboveReloadThreshold && megabytesUsed < reloadMemoryThresholdMb) {
550-
isAboveReloadThreshold = false;
551-
}
552569
break;
553570
case "open-project":
554571
handleOpenProjectCommand(req);

javascript/extractor/src/com/semmle/js/parser/TypeScriptParser.java

Lines changed: 51 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import java.util.Collections;
1818
import java.util.List;
1919
import java.util.Map;
20+
import java.util.concurrent.TimeUnit;
2021

2122
import com.google.gson.JsonArray;
2223
import com.google.gson.JsonElement;
@@ -37,7 +38,6 @@
3738
import com.semmle.util.exception.InterruptedError;
3839
import com.semmle.util.exception.ResourceError;
3940
import com.semmle.util.exception.UserError;
40-
import com.semmle.util.io.WholeIO;
4141
import com.semmle.util.logging.LogbackUtils;
4242
import com.semmle.util.process.AbstractProcessBuilder;
4343
import com.semmle.util.process.Builder;
@@ -114,6 +114,18 @@ public class TypeScriptParser {
114114
*/
115115
public static final String TYPESCRIPT_NODE_FLAGS = "SEMMLE_TYPESCRIPT_NODE_FLAGS";
116116

117+
/**
118+
* Exit code for Node.js in case of a fatal error from V8. This exit code sometimes occurs
119+
* when the process runs out of memory.
120+
*/
121+
private static final int NODEJS_EXIT_CODE_FATAL_ERROR = 5;
122+
123+
/**
124+
* Exit code for Node.js in case it exits due to <code>SIGABRT</code>. This exit code sometimes occurs
125+
* when the process runs out of memory.
126+
*/
127+
private static final int NODEJS_EXIT_CODE_SIG_ABORT = 128 + 6;
128+
117129
/** The Node.js parser wrapper process, if it has been started already. */
118130
private Process parserWrapperProcess;
119131

@@ -250,7 +262,7 @@ private void setupParserWrapper() {
250262
int mainMemoryMb =
251263
typescriptRam != 0
252264
? typescriptRam
253-
: getMegabyteCountFromPrefixedEnv(TYPESCRIPT_RAM_SUFFIX, 1000);
265+
: getMegabyteCountFromPrefixedEnv(TYPESCRIPT_RAM_SUFFIX, 2000);
254266
int reserveMemoryMb = getMegabyteCountFromPrefixedEnv(TYPESCRIPT_RAM_RESERVE_SUFFIX, 400);
255267

256268
File parserWrapper = getParserWrapper();
@@ -318,15 +330,7 @@ private JsonObject talkToParserWrapper(JsonObject request) {
318330
if (parserWrapperProcess == null) setupParserWrapper();
319331

320332
if (!parserWrapperProcess.isAlive()) {
321-
int exitCode = 0;
322-
try {
323-
exitCode = parserWrapperProcess.waitFor();
324-
} catch (InterruptedException e) {
325-
Exceptions.ignore(e, "This is for diagnostic purposes only.");
326-
}
327-
String err = new WholeIO().strictReadString(parserWrapperProcess.getErrorStream());
328-
throw new CatastrophicError(
329-
"TypeScript parser wrapper terminated with exit code " + exitCode + "; stderr: " + err);
333+
throw getExceptionFromMalformedResponse(null, null);
330334
}
331335

332336
String response = null;
@@ -335,31 +339,52 @@ private JsonObject talkToParserWrapper(JsonObject request) {
335339
toParserWrapper.newLine();
336340
toParserWrapper.flush();
337341
response = fromParserWrapper.readLine();
338-
if (response == null)
339-
throw new CatastrophicError(
340-
"Could not communicate with TypeScript parser wrapper "
341-
+ "(command: "
342-
+ parserWrapperCommand
343-
+ ").");
344-
return new JsonParser().parse(response).getAsJsonObject();
342+
if (response == null || response.isEmpty()) {
343+
throw getExceptionFromMalformedResponse(response, null);
344+
}
345+
try {
346+
return new JsonParser().parse(response).getAsJsonObject();
347+
} catch (JsonParseException | IllegalStateException e) {
348+
throw getExceptionFromMalformedResponse(response, e);
349+
}
345350
} catch (IOException e) {
346351
throw new CatastrophicError(
347352
"Could not communicate with TypeScript parser wrapper "
348353
+ "(command: ."
349354
+ parserWrapperCommand
350355
+ ").",
351356
e);
352-
} catch (JsonParseException | IllegalStateException e) {
353-
throw new CatastrophicError(
354-
"TypeScript parser wrapper sent unexpected response: "
355-
+ response
356-
+ " (command: "
357-
+ parserWrapperCommand
358-
+ ").",
359-
e);
360357
}
361358
}
362359

360+
/**
361+
* Creates an exception object describing the best known reason for the TypeScript parser wrapper
362+
* failing to behave as expected.
363+
*
364+
* Note that the stderr stream is redirected to our stderr so a more descriptive error is likely
365+
* to be found in the log, but we try to make the Java exception descriptive as well.
366+
*/
367+
private RuntimeException getExceptionFromMalformedResponse(String response, Exception e) {
368+
try {
369+
Integer exitCode = null;
370+
if (parserWrapperProcess.waitFor(1L, TimeUnit.SECONDS)) {
371+
exitCode = parserWrapperProcess.waitFor();
372+
}
373+
if (exitCode != null && (exitCode == NODEJS_EXIT_CODE_FATAL_ERROR || exitCode == NODEJS_EXIT_CODE_SIG_ABORT)) {
374+
return new ResourceError("The TypeScript parser wrapper crashed, possibly from running out of memory.", e);
375+
}
376+
if (exitCode != null) {
377+
return new CatastrophicError("The TypeScript parser wrapper crashed with exit code " + exitCode);
378+
}
379+
} catch (InterruptedException e1) {
380+
Exceptions.ignore(e, "This is for diagnostic purposes only.");
381+
}
382+
if (response == null) {
383+
return new CatastrophicError("No response from TypeScript parser wrapper", e);
384+
}
385+
return new CatastrophicError("Unexpected response from TypeScript parser wrapper:\n" + response, e);
386+
}
387+
363388
/**
364389
* Returns the AST for a given source file.
365390
*

0 commit comments

Comments
 (0)