Commit the solve behind a type query on an unopened file
Summary:
TSP supports requests on files which have not yet been opened. In this case, we
would create a throw-away transaction that solved the module to
`Step::Solutions` with `Require::Everything`, ran the request handler, and
returned the result. Solving the module for every request is extremely
expensive, but all that work is thrown away between requests. Furthermore, TSP
is fairly chatty, sending hundreds (sometimes thousands) of `getComputedType`
requests.
This change offers a simple but effective solution: when handling a request for
a non-open file, we create a "possibly committable" transaction to raise the
require level for that handle. We are either able to (1) get a committable
transaction which updates the committed state, so future requests share the
earlier solve work, or (2) we get a non-committable transaction, but the
`TransactionManager` maintains a saved state, so that multiple requests can
still share solve work even when a background recheck is happening. This is also
how the Pyrefly LSP works.
To test this change I captured and replayed a Pylance session which had 43,811
`getComputedType` requests (89% of them through the extra connection). Request
costs compared to parent, in ms:
```
request target n | before p50 p90 total | after p50 p90 total
getComputedType non-open 22294 | 16.886 57.82 667.79s | 0.067 0.114 3.43s
getComputedType open 21517 | 0.046 0.09 1.25s | 0.059 0.074 1.34s
getSnapshot - 191 | 0.069 7.18 1.14s | 0.068 6.632 1.05s
resolveImport - 994 | 0.130 1.38 0.58s | 0.087 1.229 0.42s
total 670.95s total 6.41s
```
Both runs also carried an unrelated `demand` fix that is not part of this stack,
worth roughly 0.2s of the "after" total.
Test Plan:
Answers are unchanged. Response payloads carry allocation-order identifiers
that differ between runs, so responses are compared ignoring digits: 43822 of
43873 (99.88%) match. All 51 that differ are `-32802` snapshot-outdated
responses, never a differing type. Two runs of the same binary differ
structurally in 78 responses, so the gap between the two binaries is within a
single binary's run-to-run variation.
Pyrefly is a type checker and language server for Python, which provides lightning-fast type checking along with IDE features such as code navigation, semantic highlighting, and code completion. It is available as a command-line tool and an extension for popular IDEs and editors such as VSCode, Neovim, Zed, and more.
See the Pyrefly website for full documentation and how to add Pyrefly to your editor of choice.
Pyrefly's current development status is stable.
pyrefly init, silence existing errors with pyrefly suppress, and generate type annotations with pyrefly infer. Start with one file and expand at your own pace.pip install pyreflyPyrefly releases new minor versions (1.x.0) monthly and patch versions in between as-needed for critical fixes. Pyrefly does not follow strict semantic versioning: minor versions contain more significant changes than patch versions, but any version may introduce new type errors and other breaking changes. The pyrefly suppress command can be used to easily silence errors when upgrading to a new version.
If you have questions or would like to report a bug, please create an issue.
See our contributing guide and architecture overview for information on how to contribute to Pyrefly.
Join our Discord to chat about Pyrefly and types. This is also where we hold biweekly office hours.