Skip to content

Add upper limit for the program size to prevent tsserver from crashing #7486

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 19 commits into from
Jun 16, 2016
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
4d3cff1
Add upper limit for the program size, fix readDirectory for the symli…
zhengbli Mar 11, 2016
b155fa8
Add comments
zhengbli Mar 12, 2016
a3aa000
CR feedback / Change upper limit / Add disableSizeLimit compiler option
zhengbli Mar 14, 2016
a6a466c
online and offline CR feedback
zhengbli Mar 15, 2016
d4eb3b8
Don't count current opened client file if it's TS file
zhengbli Mar 15, 2016
a839d93
Merge branch 'master' of https://github.com/Microsoft/TypeScript into…
zhengbli Mar 17, 2016
225e3b4
Speed up file searching
zhengbli Mar 17, 2016
c8e0b00
Make language service optional for a project
zhengbli Mar 17, 2016
d7e1d34
Merge branch 'master' of https://github.com/Microsoft/TypeScript into…
zhengbli Mar 18, 2016
cb46f16
Fix failed tests
zhengbli Mar 18, 2016
74e3d7b
Fix project updateing issue after editing config file
zhengbli Mar 18, 2016
1b76294
Merge branch 'master' of https://github.com/Microsoft/TypeScript into…
zhengbli Mar 24, 2016
5c9ce9e
Merge branch 'master' of https://github.com/Microsoft/TypeScript into…
zhengbli Mar 28, 2016
94d44ad
Merge branch 'master' of https://github.com/Microsoft/TypeScript into…
zhengbli Jun 9, 2016
d387050
Fix merging issues and multiple project scenario
zhengbli Jun 9, 2016
4383f1a
Refactoring
zhengbli Jun 9, 2016
e41b10b
add test and spit commandLineParser changes to another PR
zhengbli Jun 10, 2016
3354436
Merge branch 'master' of https://github.com/Microsoft/TypeScript into…
zhengbli Jun 15, 2016
550d912
Refactor code to make if statements cheaper
zhengbli Jun 15, 2016
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/compiler/commandLineParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -566,7 +566,7 @@ namespace ts {
}
else {
// by default exclude node_modules, and any specificied output directory
exclude = ["node_modules"];
exclude = ["node_modules", "bower_components"];
const outDir = json["compilerOptions"] && json["compilerOptions"]["outDir"];
if (outDir) {
exclude.push(outDir);
Expand Down
4 changes: 4 additions & 0 deletions src/compiler/diagnosticMessages.json
Original file line number Diff line number Diff line change
Expand Up @@ -2824,5 +2824,9 @@
"Unknown typing option '{0}'.": {
"category": "Error",
"code": 17010
},
"Too many javascript files in the project. Consider add to the `exclude` list in the config file.": {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Grammar: "Consider adding to...". Or perhaps just reworded as "Use the 'exclude' setting in project configuration to limit included source folders".

Note: Do we predicate this message on the user not having listed the files to compile? If we are not scanning directories anyway (i.e. there is a list of files given), then this message would be misleading (as exclude can't be specified - at least until the globbing change comes in).

Also, capitalize as "JavaScript", not "javascript", and don't use backticks. This isn't markdown :-p

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that we should mention both cases, with or without the "exclude" list. Does the following sound better:

"Too many JavaScript files in the project. Use an exact 'files' list, or use the 'exclude' setting in project configuration to limit included source folders."

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like it :-)

"category": "Error",
"code": 17012
}
}
16 changes: 15 additions & 1 deletion src/compiler/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -748,7 +748,21 @@ namespace ts {
}

if (!tryReuseStructureFromOldProgram()) {
forEach(rootNames, name => processRootFile(name, /*isDefaultLib*/ false));
let programSize = 0;
for (const name of rootNames) {
const path = toPath(name, currentDirectory, getCanonicalFileName);
if (programSize <= maxProgramSize) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We discussed a compiler option to disable the size check for cases where the project is legitimately large. I don't see that option in this code review.

processRootFile(name, /*isDefaultLib*/ false);
if (!hasTypeScriptFileExtension(name) && filesByName.get(path)) {
programSize += filesByName.get(path).text.length;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking in findSourceFile (https://github.com/Microsoft/TypeScript/blob/master/src/compiler/program.ts#L1466) I believe the filename can be present even if no file was found/read, thus you might want to check the text value is present before trying to access its length.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch!

}
}
else {
programDiagnostics.add(createCompilerDiagnostic(Diagnostics.Too_many_javascript_files_in_the_project_Consider_add_to_the_exclude_list_in_the_config_file));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought we were going to indicate the path to the file being processed when the limit was exceeded (as this is likely in whatever folder you should have been excluding)?

break;
}
}

// Do not process the default library if:
// - The '--noLib' flag is used.
// - A 'no-default-lib' reference comment is encountered in
Expand Down
15 changes: 9 additions & 6 deletions src/compiler/sys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ namespace ts {
watchDirectory?(path: string, callback: DirectoryWatcherCallback, recursive?: boolean): FileWatcher;
};

export var sys: System = (function () {
export var sys: System = (function() {

function getWScriptSystem(): System {

Expand Down Expand Up @@ -404,8 +404,8 @@ namespace ts {
const watchedFileSet = createWatchedFileSet();

function isNode4OrLater(): boolean {
return parseInt(process.version.charAt(1)) >= 4;
}
return parseInt(process.version.charAt(1)) >= 4;
}

const platform: string = _os.platform();
// win32\win64 are case insensitive platforms, MacOS (darwin) by default is also case insensitive
Expand Down Expand Up @@ -500,7 +500,10 @@ namespace ts {
for (const current of files) {
const name = combinePaths(path, current);
if (!contains(exclude, getCanonicalPath(name))) {
const stat = _fs.statSync(name);
// fs.statSync would throw an exception if the file is a symlink
// whose linked file doesn't exist. fs.lstatSync would return a stat
// object for the symlink file itself in this case
const stat = _fs.lstatSync(name);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like a non-trivial change with a lot of nuance (for example, this means we won't traverse into directories now if they are created as symbolic links, as isDirectory() returns false on a lstat where it used to return true on a stat.

How is this part of setting a limit on file size when loading? Seems like this is a different issue and should be coded/evaluated separately.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was an issue that caused tsserver to crash when editing the repro sample. The ember server generates tons of symlink files in the tmp folder pointing to some other locations, and then deletes the actual linked files after the server shuts down. Then these symlink files became invalid ones, which caused fs.statSync to throw exceptions when we try to scan the folder.

The alternative solution was to try fs.statSync first, if an exception was thrown just skip this file.

if (stat.isFile()) {
if (!extension || fileExtensionIs(name, extension)) {
result.push(name);
Expand Down Expand Up @@ -532,7 +535,7 @@ namespace ts {
// and https://github.com/Microsoft/TypeScript/issues/4643), therefore
// if the current node.js version is newer than 4, use `fs.watch` instead.
const watchSet = isNode4OrLater() ? watchedFileSet : pollingWatchedFileSet;
const watchedFile = watchSet.addFile(filePath, callback);
const watchedFile = watchSet.addFile(filePath, callback);
return {
close: () => watchSet.removeFile(watchedFile)
};
Expand Down Expand Up @@ -562,7 +565,7 @@ namespace ts {
}
);
},
resolvePath: function (path: string): string {
resolvePath: function(path: string): string {
return _path.resolve(path);
},
fileExists,
Expand Down
6 changes: 6 additions & 0 deletions src/compiler/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2484,6 +2484,10 @@ namespace ts {
return forEach(supportedJavascriptExtensions, extension => fileExtensionIs(fileName, extension));
}

export function hasTypeScriptFileExtension(fileName: string) {
return forEach(supportedTypeScriptExtensions, extension => fileExtensionIs(fileName, extension));
}

/**
* Replace each instance of non-ascii characters by one, two, three, or four escape sequences
* representing the UTF-8 encoding of the character, and return the expanded char code list.
Expand Down Expand Up @@ -2866,4 +2870,6 @@ namespace ts {
export function isParameterPropertyDeclaration(node: ParameterDeclaration): boolean {
return node.flags & NodeFlags.AccessibilityModifier && node.parent.kind === SyntaxKind.Constructor && isClassLike(node.parent.parent);
}

export const maxProgramSize = 35 * 1024 * 1024;
}
35 changes: 30 additions & 5 deletions src/server/editorServices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1217,13 +1217,35 @@ namespace ts.server {
}
else {
const project = this.createProject(configFilename, projectOptions);
let programSize = 0;

// As the project openning might not be complete if there are too many files,
// therefore to surface the diagnostics we need to make sure the given client file is opened.
if (clientFileName) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i thought we said we did not need this anymore? is this not the case?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The file reading here doesn't have an upper limit, therefore it could be much more than 20M. In the repro case it is around 100M, and in testing I noticed server crashes without the limit. So I kept the limit here as well.

const currentClientFileInfo = this.openFile(clientFileName, /*openedByClient*/ true);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you not need to worry about the this.host.fileExists check that used to be performed for all files? (i.e. is the clientFileName guaranteed to exit)?

project.addRoot(currentClientFileInfo);
programSize += currentClientFileInfo.content.length;
}

for (const rootFilename of projectOptions.files) {
if (this.host.fileExists(rootFilename)) {
const info = this.openFile(rootFilename, /*openedByClient*/ clientFileName == rootFilename);
project.addRoot(info);
if (rootFilename === clientFileName) {
continue;
}

if (programSize <= maxProgramSize) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm trying to understand why we need to repeat this check for the size of the content loaded here as well as in createProgram. It seems dangerous to have similar logic in both places they may behave slightly differently (or diverge unknowingly over time). What is the effect of not having this check here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because tsserver opens up files before calling the createProgram via compilerService. And its openFile function does some pretty complex logic for file version management. In the createProgram, the compiler calls findSourceFile function, which will load the file if it found the file wasn't loaded. Therefore I need to set the limit on both sides. I didn't find an more optimized solution to unify them yet.

if (this.host.fileExists(rootFilename)) {
const info = this.openFile(rootFilename, /*openedByClient*/ false);
project.addRoot(info);
if (!hasTypeScriptFileExtension(rootFilename)) {
programSize += info.content.length;
}
}
else {
return { errorMsg: "specified file " + rootFilename + " not found" };
}
}
else {
return { errorMsg: "specified file " + rootFilename + " not found" };
break;
}
}
project.finishGraph();
Expand Down Expand Up @@ -1251,7 +1273,10 @@ namespace ts.server {
return error;
}
else {
const oldFileNames = project.compilerService.host.roots.map(info => info.fileName);
// if the project is too large, the root files might not have been all loaded if the total
// program size reached the upper limit. In that case project.projectOptions.files should
// be more precise. However this would only happen for configured project.
const oldFileNames = project.projectOptions ? project.projectOptions.files : project.compilerService.host.roots.map(info => info.fileName);
const newFileNames = projectOptions.files;
const fileNamesToRemove = oldFileNames.filter(f => newFileNames.indexOf(f) < 0);
const fileNamesToAdd = newFileNames.filter(f => oldFileNames.indexOf(f) < 0);
Expand Down