Skip to content

Investigate making the binding phase lazy #35120

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

Open
DanielRosenwasser opened this issue Nov 15, 2019 · 3 comments
Open

Investigate making the binding phase lazy #35120

DanielRosenwasser opened this issue Nov 15, 2019 · 3 comments
Labels
Domain: Performance Reports of unusually slow behavior

Comments

@DanielRosenwasser
Copy link
Member

Today, any semantic or language service operations must be preceded by a phase of our compiler called binding. This phase does two things:

  • creates symbol tables as well as symbols per scope
  • sets parent pointers (because it's already walking the tree anyhow)

However, this can end up being a of unnecessary up-front work. For type-checking a given file, the only files that need to be bound are

  • files that affect global namespaces (e.g. global files or files containing module augmentations, global augmentations, and UMD namespaces)
  • any file that needs to be checked to check the current file

Recently, I spent a bit of time on a plane ride wondering if we could do less work based on this. Instead of forcing all files to be bound, we could bind only global-affecting files up-front, and then force a bind prior to checking or resolving a given file. This has the advantage that something like quick info only needs to bind the minimal set of dependencies before coming back with an answer, making checking significantly lazier. It also means that skipLibCheck could end up working faster in command-line scenarios by binding fewer .d.ts files that are automatically included (e.g. why bind .d.ts files for Jest if you're compiling app code instead of test code?).

The flip side of this is that making this lazy can complicate a lot of other operations. Many language service operations don't actually care about binding, but they do care about parent pointers being set. They'll be preceded by a call to getTypeChecker() just to ensure files are bound before performing specific steps.

Yeah, I know, weird design!

The other issue is that certain type-checker APIs likely need to be guarded against to ensure a requested file is bound. I haven't dived deep here, so this is more of a speculative concern.

Finally, while laziness means that we can partially amortize each operation into incremental chunks of work, there's no telling when pulling on a thread of work will trigger TypeScript to do ALL of the work. Currently TypeScript does ALL the work up front, but that might be good for avoiding frustrating delays later on. For example, if not all files are bound yet, TypeScript can't immediately respond to go to symbol, find all references, or even some cases of get completions (thanks to auto-imports!) before ensuring every file is bound.

On the other hand, once that work gets done, it's done! Only re-parsed files need to be re-bound. So TypeScript might start out slow on some operations, warming up, and eventually staying hot going forward. There are also other possibilities of making this easier. For example, the services layer could also potentially bind unbound files in the background on idle time if it turned out we really needed to.

@DavidANeil
Copy link

I was investigating adding support for a Bazel option (unused_inputs_list bazel-contrib/rules_nodejs#968) that lets a tool output a list of files that it was given as dependencies, but never used, which helps prevent unnecessary rebuilds.
I've learned that this list is empty for the TypeScript compiler, as every file passed in the "files" attribute of tsconfig.json is read and parsed, even if it is never depended on by any of the actual source files.

Not only would I think this would be a huge performance gain (quick back of the napkin estimates our full build would require ~1,500,000 fewer file bindings).

This would drastically expand the scope, but if it were possible to specify in the tsconfig "files" input which files are of the "global augmenting" variety, then it would be great to not even read the other ones from the file system. This would be another large performance gain (I'd estimate this one closer to ~1,000,000 fewer file system reads and parses), and it would also enable much better caching between builds in Bazel.

@DanielRosenwasser
Copy link
Member Author

I'm not totally sure if I've understood how this helps, but I should try to give some background.

This optimization was mostly targeted at language service operations so that type-checking operations could do less work (e.g. quick info, requesting errors, etc.) - if you're never checking another file, you never need to set up scopes in that file.

But to understand the transitive closure of files that a program forms, that involves something totally independent - program construction. Program construction needs to happen regardless of type-checking to get a complete picture of the world, and it doesn't require binding files at all.

If you have a custom tool that knows what that picture of the world is, you might want to try playing with --noResolve. If you need to know about all files without binding/checking/emitting, try `--listFilesOnly.

Additionally, binding often is not the bottleneck in most compilations that we've seen - try running with --extendedDiagnostics on a project to get a sense of how much you're losing to binding.

@DavidANeil
Copy link

DavidANeil commented Mar 30, 2020

I wasn't able to get extendedDiagnostics to do anything from the compiler api, but I put in some debuggers and grabbed ts.performance.getDuration("Bind"); and it looks like binding isn't taking all that much time, about 80ms for 1500 files.
I will create a new issue for discussing optimizations to Program construction.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Domain: Performance Reports of unusually slow behavior
Projects
None yet
Development

No branches or pull requests

2 participants