Skip to content

Commit a0021f3

Browse files
committed
TS: do more work in parallel
1 parent dd6fd40 commit a0021f3

File tree

4 files changed

+129
-17
lines changed

4 files changed

+129
-17
lines changed

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

Lines changed: 56 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -67,13 +67,24 @@ interface ResetCommand {
6767
interface QuitCommand {
6868
command: "quit";
6969
}
70+
interface PrepareFilesCommand {
71+
command: "prepare-files";
72+
filenames: string[];
73+
}
7074
type Command = ParseCommand | OpenProjectCommand | CloseProjectCommand
71-
| GetTypeTableCommand | ResetCommand | QuitCommand;
75+
| GetTypeTableCommand | ResetCommand | QuitCommand | PrepareFilesCommand;
7276

7377
/** The state to be shared between commands. */
7478
class State {
7579
public project: Project = null;
7680
public typeTable = new TypeTable();
81+
82+
/** List of files that have been requested. */
83+
public pendingFiles: string[] = [];
84+
public pendingFileIndex = 0;
85+
86+
/** Next response to be delivered. */
87+
public pendingResponse: string = null;
7788
}
7889
let state = new State();
7990

@@ -154,16 +165,43 @@ function getSourceCode(filename: string): string {
154165
return code;
155166
}
156167

157-
function handleParseCommand(command: ParseCommand) {
158-
let filename = String(command.filename);
168+
function extractFile(filename: string): string {
159169
let {ast, code} = getAstForFile(filename);
160170

161171
// Get the AST and augment it.
162172
ast_extractor.augmentAst(ast, code, state.project);
163173

164-
console.log(stringifyAST(
165-
{ type: "ast", ast, nodeFlags: ts.NodeFlags, syntaxKinds: ts.SyntaxKind },
166-
));
174+
return stringifyAST({
175+
type: "ast",
176+
ast,
177+
nodeFlags: ts.NodeFlags,
178+
syntaxKinds: ts.SyntaxKind
179+
});
180+
}
181+
182+
function prepareNextFile() {
183+
if (state.pendingResponse != null) return;
184+
if (state.pendingFileIndex < state.pendingFiles.length) {
185+
let nextFilename = state.pendingFiles[state.pendingFileIndex];
186+
state.pendingResponse = extractFile(nextFilename);
187+
}
188+
}
189+
190+
function handleParseCommand(command: ParseCommand) {
191+
let filename = command.filename;
192+
let expectedFilename = state.pendingFiles[state.pendingFileIndex];
193+
if (expectedFilename !== filename) {
194+
throw new Error("File requested out of order. Expected '" + expectedFilename + "' but got '" + filename + "'");
195+
}
196+
++state.pendingFileIndex;
197+
let response = state.pendingResponse || extractFile(command.filename);
198+
state.pendingResponse = null;
199+
process.stdout.write(response + "\n", () => {
200+
// Start working on the next file as soon as the old one is flushed.
201+
// Note that if we didn't wait for flushing, this would block the I/O
202+
// loop and delay flushing.
203+
prepareNextFile();
204+
});
167205
}
168206

169207
/**
@@ -360,6 +398,15 @@ function handleResetCommand(command: ResetCommand) {
360398
}));
361399
}
362400

401+
function handlePrepareFilesCommand(command: PrepareFilesCommand) {
402+
state.pendingFiles = command.filenames;
403+
state.pendingFileIndex = 0;
404+
state.pendingResponse = null;
405+
process.stdout.write('{"type":"ok"}\n', () => {
406+
prepareNextFile();
407+
});
408+
}
409+
363410
function reset() {
364411
state = new State();
365412
state.typeTable.restrictedExpansion = getEnvironmentVariable("SEMMLE_TYPESCRIPT_NO_EXPANSION", Boolean, false);
@@ -400,6 +447,9 @@ function runReadLineInterface() {
400447
case "get-type-table":
401448
handleGetTypeTableCommand(req);
402449
break;
450+
case "prepare-files":
451+
handlePrepareFilesCommand(req);
452+
break;
403453
case "reset":
404454
handleResetCommand(req);
405455
break;

javascript/extractor/src/com/semmle/js/extractor/AutoBuild.java

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -467,27 +467,49 @@ public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) th
467467
logEndProcess();
468468
// Extract all files belonging to this project which are also matched
469469
// by our include/exclude filters.
470+
List<File> typeScriptFiles = new ArrayList<File>();
470471
for (File sourceFile : project.getSourceFiles()) {
471472
Path sourcePath = sourceFile.toPath();
472473
if (!filesToExtract.contains(normalizePath(sourcePath)))
473474
continue;
474475
if (extractedFiles.add(sourcePath)) {
475-
extract(extractor, sourcePath);
476+
typeScriptFiles.add(sourcePath.toFile());
476477
}
477478
}
479+
tsParser.prepareFiles(typeScriptFiles);
480+
for (File file : typeScriptFiles) {
481+
extract(extractor, file.toPath());
482+
}
478483
tsParser.closeProject(projectFile);
479484
}
480485

481486
if (!tsconfigFiles.isEmpty()) {
482487
// Extract all the types discovered when extracting the ASTs.
483488
TypeTable typeTable = tsParser.getTypeTable();
484489
extractTypeTable(tsconfigFiles.iterator().next(), typeTable);
490+
}
485491

486-
// The TypeScript compiler instance is no longer needed.
487-
tsParser.killProcess();
492+
// Extract remaining TypeScript files.
493+
List<File> remainingTypeScriptFiles = new ArrayList<File>();
494+
for (Path f : filesToExtract) {
495+
if (!extractedFiles.contains(f) && FileType.forFileExtension(f.toFile()) == FileType.TYPESCRIPT) {
496+
remainingTypeScriptFiles.add(f.toFile());
497+
}
488498
}
499+
if (!remainingTypeScriptFiles.isEmpty()) {
500+
tsParser.prepareFiles(remainingTypeScriptFiles);
501+
for (File f : remainingTypeScriptFiles) {
502+
Path path = f.toPath();
503+
if (extractedFiles.add(path)) {
504+
extract(extractor, path);
505+
}
506+
}
507+
}
508+
509+
// The TypeScript compiler instance is no longer needed.
510+
tsParser.killProcess();
489511

490-
// Extract files that were not part of a project.
512+
// Extract non-TypeScript files
491513
for (Path f : filesToExtract) {
492514
if (extractedFiles.add(f)) {
493515
extract(extractor, f);

javascript/extractor/src/com/semmle/js/extractor/Main.java

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22

33
import java.io.File;
44
import java.io.IOException;
5+
import java.util.ArrayList;
56
import java.util.LinkedHashSet;
7+
import java.util.List;
68
import java.util.Set;
79
import java.util.regex.Pattern;
810

@@ -140,16 +142,22 @@ public void run(String[] args) {
140142
tsParser.verifyInstallation(!ap.has(P_QUIET));
141143
}
142144
for (File projectFile : projectFiles) {
145+
143146
long start = verboseLogStartTimer(ap, "Opening project " + projectFile);
144147
ParsedProject project = tsParser.openProject(projectFile);
145148
verboseLogEndTimer(ap, start);
146149
// Extract all files belonging to this project which are also matched
147150
// by our include/exclude filters.
151+
List<File> filesToExtract = new ArrayList<>();
148152
for (File sourceFile : project.getSourceFiles()) {
149-
if (files.contains(normalizeFile(sourceFile))) {
150-
ensureFileIsExtracted(sourceFile, ap);
153+
if (files.contains(normalizeFile(sourceFile)) && !extractedFiles.contains(sourceFile.getAbsoluteFile())) {
154+
filesToExtract.add(sourceFile);
151155
}
152156
}
157+
tsParser.prepareFiles(filesToExtract);
158+
for (int i = 0; i < filesToExtract.size(); ++i) {
159+
ensureFileIsExtracted(filesToExtract.get(i), ap);
160+
}
153161
// Close the project to free memory. This does not need to be in a `finally` as
154162
// the project is not a system resource.
155163
tsParser.closeProject(projectFile);
@@ -159,14 +167,27 @@ public void run(String[] args) {
159167
// Extract all the types discovered when extracting the ASTs.
160168
TypeTable typeTable = tsParser.getTypeTable();
161169
extractTypeTable(projectFiles.iterator().next(), typeTable);
170+
}
162171

163-
// The TypeScript compiler instance is no longer needed - free up some memory.
164-
if (hasSharedExtractorState) {
165-
tsParser.reset(); // This is called from a test runner, so keep the process alive.
166-
} else {
167-
tsParser.killProcess();
172+
List<File> remainingTypescriptFiles = new ArrayList<>();
173+
for (File f : files) {
174+
if (!extractedFiles.contains(f.getAbsoluteFile()) && FileType.forFileExtension(f) == FileType.TYPESCRIPT) {
175+
remainingTypescriptFiles.add(f);
168176
}
169177
}
178+
if (!remainingTypescriptFiles.isEmpty()) {
179+
tsParser.prepareFiles(remainingTypescriptFiles);
180+
for (File f : remainingTypescriptFiles) {
181+
ensureFileIsExtracted(f, ap);
182+
}
183+
}
184+
185+
// The TypeScript compiler instance is no longer needed - free up some memory.
186+
if (hasSharedExtractorState) {
187+
tsParser.reset(); // This is called from a test runner, so keep the process alive.
188+
} else {
189+
tsParser.killProcess();
190+
}
170191

171192
// Extract files that were not part of a project.
172193
for (File f : files) {

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,25 @@ public Result parse(File sourceFile, String source) {
292292
}
293293
}
294294

295+
/**
296+
* Informs the parser process that the following files are going to be
297+
* requested, in that order.
298+
* <p>
299+
* The parser process uses this list to start work on the next file before it is
300+
* requested.
301+
*/
302+
public void prepareFiles(List<File> files) {
303+
JsonObject request = new JsonObject();
304+
request.add("command", new JsonPrimitive("prepare-files"));
305+
JsonArray filenames = new JsonArray();
306+
for (File file : files) {
307+
filenames.add(new JsonPrimitive(file.getAbsolutePath()));
308+
}
309+
request.add("filenames", filenames);
310+
JsonObject response = talkToParserWrapper(request);
311+
checkResponseType(response, "ok");
312+
}
313+
295314
/**
296315
* Opens a new project based on a tsconfig.json file. The compiler will analyze
297316
* all files in the project.

0 commit comments

Comments
 (0)