Skip to content

perf(workspace): cache the walk of a document for hover, inlay hints and semantic tokens - #930

Open
psteinroe wants to merge 1 commit into
feat/sql-function-bodiesfrom
perf/editor-walk-cache
Open

psteinroe wants to merge 1 commit into
feat/sql-function-bodiesfrom
perf/editor-walk-cache

Conversation

@psteinroe

Copy link
Copy Markdown
Collaborator

Stacked on #918.

Hover, inlay hints and semantic tokens walked the file from the first statement and resolved every statement again on every request. Nothing was cached or shared, so on long files each request after an edit took seconds, and the editor sends several of them per edit.

What changed

  • The walk is cached per document (server/editor_walk.rs, WorkspaceServer::walk_references):
    • Every max(64, statements / 32) top-level statements it keeps a checkpoint of the catalog, the session and the migration section state. Each checkpoint is keyed by a hash of the statements before it and their sections (by position in the section list, direction and transaction mode, not by range, so edits before a section don't invalidate it).
    • It keeps the references of each resolved statement, keyed by the hash of the statements before its top-level statement plus its own id (which includes the function a body statement belongs to), with the body statements of each top-level statement.
    • A request whose statements are all cached is answered from the cache without parsing anything. Otherwise the walk resumes from the last valid checkpoint before the range, so an edit only costs the statements since that checkpoint.
    • The cache holds the snapshot it was built from and is reset when the snapshot or the search path changes, on invalidate_schema_cache, and when the file is closed. Stale entries are pruned once there are more than twice as many as the document has statements.
  • walk_statements is gone. references_at, inlay hints and semantic tokens use the cached walk. pgls_inlay_hints::inlay_hints_of_references and pgls_semantic_tokens::semantic_tokens_of_references compute hints and tokens from the cached references instead of resolving again. The old entry points remain as wrappers that resolve first.
  • Inlay hints: the call's name token is found with a binary search instead of a linear one, and calls of functions without named parameters (most built-ins) are skipped before scanning the statement.
  • Document::iter_from and Document::statement_ranges let the walk start at a top-level statement.

Timings

Release build, best of 3, on a loaded shared machine. The files are the migration-style benchmark from the analysis: per group a table, an index, a SQL function and a query. The benchmark is in server.perf_tests.rs (cargo test --release -p pgls_workspace --lib perf_tests -- --ignored --nocapture --test-threads=1). "Before" is the same benchmark on #918, where every request costs the same. "Cold" is the first request after clearing the cache, "warm" a repeated request.

request (60 visible lines unless noted) statements before after, cold after, warm
inlay hints at the end 1,250 105 ms 22 ms 0.1 ms
5,000 336 ms 438 ms 0.3 ms
20,000 1,700 ms 1,208 ms 0.8 ms
semantic tokens range at the end 1,250 106 ms 32 ms 0.1 ms
5,000 327 ms 346 ms 0.3 ms
20,000 1,903 ms 1,212 ms 1.0 ms
semantic tokens full 1,250 114 ms 157 ms 0.6 ms
5,000 446 ms 834 ms 7 ms
20,000 2,053 ms 2,516 ms 25 ms
hover: references at the end 20,000 1,343 ms 1,187 ms 0.8 ms
edit the last statement, then tokens + hints at the end 1,250 ~211 ms 2.4 ms
5,000 ~663 ms 23 ms
20,000 ~3,600 ms 71 ms
one select with 3,000 calls: tokens range 1 388 ms 447 ms 1.0 ms

"Before" for the edit row is the sum of the two requests, since nothing is cached.

What this doesn't fix

  • The first request on a file still walks everything before its range, and is up to about 20–80% slower for the whole file because it saves the checkpoints. Most of that time is re-parsing: the 1,000-entry statement caches thrash on files this long, which is being fixed on main and will speed up the cold path.
  • Hover as a whole stays slow on long files (1.7 s warm at 20,000 statements): on_hover still iterates the whole document with .last() and builds snapshot_before. Both are being fixed on main; with that, hover gets the warm references_at time above.
  • Code actions are unchanged (they run the whole linter).
  • Memory: up to about 32 copies of the catalog per open document, plus the references of the statements that were requested.

Tests

test_editor_walk_cache_follows_edits: a 300-statement file (so checkpoints exist), tokens and hints for the tail and the whole file after renaming a column before, inside and after the cached part, compared with a fresh workspace each time. Breaking the hash checks makes it fail. All existing hover, inlay hint, semantic token and LSP tests pass.

Conflicts

ParseIterator, Document and hover are also changed on main (iterator recursion, statement caches, hover's double walk). Expect conflicts in document.rs and in on_hover/references_at. This also conflicts with #904 in pgls_semantic_tokens/src/lib.rs (the top of semantic_tokens).

…and semantic tokens

Hover, inlay hints and semantic tokens walked the file from the start
and resolved every statement again on each request. The walk is now
cached per document: a checkpoint of the catalog and the session every
few statements, and the references of each resolved statement, keyed by
a hash of the statements before it. A request resolves at most the
statements since the last valid checkpoint before its range, and a
repeated request only looks the references up. Inlay hints and semantic
tokens are computed from the cached references.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant