Skip to content

perf(workspace): keep every statement of a document cached and walk once for hover - #913

Open
psteinroe wants to merge 1 commit into
fix/statement-iterator-stack-overflowfrom
perf/document-statement-caches
Open

psteinroe wants to merge 1 commit into
fix/statement-iterator-stack-overflowfrom
perf/document-statement-caches

Conversation

@psteinroe

@psteinroe psteinroe commented Oct 24, 2026 •

Copy link
Copy Markdown
Collaborator

Stacked on #892.

On files with more than a few hundred statements, hover, inlay hints and code actions got much slower than the file size explains. Two causes:

  • Each document caches the parse result, the tree-sitter tree and the annotations of its statements in LRU caches of 1,000 entries, and the body of each SQL function takes an entry of its own. Once a file has more statements than that, a walk from the top evicts the entries it needs next, so every walk parses every statement again.
  • Hover walked the statements before the cursor twice. references_at walks them to resolve the hovered statement. Then snapshot_before iterated them again with ExecuteStatementMapper to hash their text for its cache key, which deep-cloned every AST even on a cache hit. On a miss it re-applied all the DDL to a fresh catalog. The cache had one entry, so moving the cursor to another statement always missed.

What changed

  • PgQueryStore, TreeSitterStore and AnnotationStore use plain maps. Document::update_content drops the entries of statements that are no longer in the document. SQL function bodies are kept as long as their CREATE FUNCTION is. The cache can't hold more than the document's own statements, so it doesn't need a size limit. lru is no longer a dependency of pgls_workspace.
  • Hover walks once. walk_statements returns its catalog, and hover builds the snapshot from the catalog that the hovered statement sees during that walk. A statement that doesn't parse isn't visited, so it gets the catalog at the end of the walk, which is the same catalog.
  • When all references under the cursor are to objects that the file didn't change (Origin::Database, or columns of CTEs and subqueries), hover uses the database snapshot and doesn't build one.
  • The snapshot cache key is a hash of the text before the statement, plus the CREATE FUNCTION for a function body, together with the database snapshot it was built from. Hashing the text is cheap and needs no walk, and edits inside or after the statement keep the cached snapshot. The cache is cleared when the settings change, because the search path and migration sections change what a statement sees. Completions use the same cache and only walk on a miss.

A side effect: hover and completions now follow migration sections the way the analyser and inlay hints do. The old snapshot_before ignored sections.

Timings

Release build, best of 3, on a shared and loaded machine, so the numbers are noisy. Measured with the probe from the release analysis. The file has groups of a table, an index, a SQL function and a query; "@EnD" is the last query of the file. "Hover, moving" hovers a different statement on each run, so the snapshot cache misses every time.

5,000 lines (6,250 statements) before → after 20,000 lines (25,000 statements) before → after
iterate the document 298 → 64 ms 1,347 → 402 ms
walk the statements 334 → 108 ms 1,695 → 456 ms
inlay hints (60 lines) @EnD 402 → 184 ms 3,792 → 341 ms
hover on a column @EnD 885 → 194 ms 2,993 → 320 ms
hover, moving @EnD 961 → 313 ms 4,085 → 1,014 ms
code actions @EnD 955 → 1,095 ms 3,557 → 2,576 ms

Hover and inlay hints at the top of a file were already fast (under 10 ms) and stay that way. Code actions are dominated by the lint run on every request, which is a separate PR.

Tests

  • keeps_the_parsed_statements_that_are_still_in_the_document: after an edit, the parse result and tree of an unchanged statement are the same Arc, and a removed statement is parsed again.
  • hover_follows_the_ddl_before_the_statement: hover on a table created in the file, on a database table that the file alters (the snapshot is built), on a database table the file doesn't touch (the database snapshot is used), and again after the CREATE TABLE above the hovered statement changes (the cache key changes).
  • The existing hover, completion and inlay-hint tests pass unchanged.

Risks and not covered

  • Memory: a document now keeps the AST and tree-sitter tree of every one of its statements, where it used to keep at most 1,000. Entries are dropped when a statement is edited away or the document is closed.
  • Each request still walks every statement before its range. That walk is now the main cost of hover and inlay hints near the end of a big file (about 350 ms at 25,000 statements). A cached, checkpointed walk shared between requests is left for a follow-up. Building the snapshot after a cursor move (about 500 ms on the biggest file, mostly cloning the database snapshot) is also still there.
  • A hover whose references are all unchanged database objects, with the cursor on a different kind of name than any reference (e.g. the schema qualifier of a database table), falls back to looking up the name in the database snapshot rather than in the file's snapshot. The result only differs if the file created or dropped an object with that name.

Follow-ups: The cached walk shared between requests is #930 (on the #888 stack). The memory of these caches is bounded in #951.

…nce for hover

The parse and tree-sitter caches of a document held 1,000 statements, so
on bigger files every walk over the document parsed every statement again.
They now keep the statements of the document and drop the ones that are
gone when the content changes.

Hover walked the statements before the cursor twice: once to resolve the
references, and once more in snapshot_before, which also deep-cloned
every AST. It now builds the snapshot from the catalog of its own walk,
skips it when the hovered references are all to objects the file didn't
change, and caches it by the text before the statement.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant