Skip to content

perf(analyser): share statement trees with the workspace instead of copying them - #965

Closed
psteinroe wants to merge 1 commit into
perf/incremental-statement-splitfrom
perf/analyser-shared-asts
Closed

psteinroe wants to merge 1 commit into
perf/incremental-statement-splitfrom
perf/analyser-shared-asts

Conversation

@psteinroe

@psteinroe psteinroe commented Oct 24, 2026 •

Copy link
Copy Markdown
Collaborator

Stacked on #964.

#951 measured a transient peak of 265 MiB for analysing a single 5,000-statement file, about twice what its cached parse trees take. The workspace cached each statement's tree behind an Arc, but every analysis made two copies of it:

  • AnalyserDiagnosticsMapper deep-cloned each tree into AnalysableStatement::root. That mapper is also what walk_statements uses, so hover and inlay hints copied every statement before the cursor too.
  • Analyser::analyse cloned all roots again into a Vec<NodeEnum> for AnalysedFileContext.

The CLI runs one file per rayon worker, so the peak is multiplied by the number of workers.

What changed

  • AnalysableStatement::root is an Arc<NodeEnum>, and AnalysableStatement::new(root: impl Into<Arc<NodeEnum>>, range) still takes an owned tree. That is the only public API change: callers that build statements from owned trees (the rules tests, the regress test) compile unchanged.
  • AnalysedFileContext::stmts and previous_stmts() are &[&NodeEnum]. The rules that look at other statements match through the reference unchanged.
  • The workspace caches Result<Arc<NodeEnum>, SyntaxDiagnostic> (pg_query::ParseResult), and all statement mappers hand out the shared Arc. That includes TypecheckDiagnosticsMapper, which also deep-cloned the whole CREATE FUNCTION for each statement in its body.
  • The formatter is the only place that changes a tree, when it puts a formatted SQL function body back. It uses Arc::make_mut, which copies only that function's tree.

Measurements

tests/memory_bounds.rs, release build, 20 documents × 5,000 statements. The test now also prints how long the CLI pass takes (open, diagnostics, close for each file).

before after
peak for one document 264.9 MiB 142.6 MiB
peak with all documents open 425.1 MiB 297.8 MiB
retained with all documents open 161.3 MiB 161.8 MiB
CLI: peak, one file at a time 265.0 MiB 137.2 MiB
CLI: diagnostics for 20 files 36.9 s 24.8 s

What is retained doesn't change, since it is the cache itself. The peaks now stay close to it, and dropping the copies also makes the analysis about a third faster.

Tests

The existing suites cover it:

  • pgls_analyser: rules specs and the Postgres regression test;
  • pgls_workspace, including formatting of SQL function bodies through make_mut, and the memory and edit-churn tests;
  • pgls_lsp.

Not covered

pgls_typecheck's EXPLAIN path and the formatter take &NodeEnum and don't copy. I didn't look for copies outside the workspace and the analyser.

…opying them

The workspace copied the parse tree of every statement into the analyser
(AnalysableStatement owned its tree), and the analyser copied them all
once more for the file context. Analysing a file of 5,000 statements
peaked at twice its cached trees. AnalysableStatement::root is now an Arc
of the tree that the workspace caches, and the file context borrows them.
The other statement mappers share the trees too; the formatter copies a
tree only when it replaces a function body in it.
@psteinroe

Copy link
Copy Markdown
Collaborator Author

Superseded by #1002 (rebuild of the document caches and walk).

@psteinroe psteinroe closed this Oct 26, 2026
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