Repository navigation
Conversation
A CacheBudget across documents counts what each document's cache keeps (parse results, trees, resolved statements and walk checkpoints) after each request, and drops the caches of the least recently used documents when all of them keep more than DOCUMENT_CACHE_ENTRIES_MAX. Closing a document drops its caches. Two tests count the memory: open documents stay within the budget and nothing is left after closing them, and a long editing session doesn't grow the caches.
This was referenced Oct 26, 2026
# Conflicts: # crates/pgls_workspace/src/workspace/server/document.rs # crates/pgls_workspace/src/workspace/server/document_cache.rs
This was referenced Oct 26, 2026
edjubert
reviewed
Oct 27, 2026
Comment on lines
+200
to
+209
| for evicted in self.document_caches.record(path, doc.cache().usage()) { | ||
| if let Some(doc) = documents.get(&evicted) { | ||
| tracing::debug!( | ||
| "Dropping the caches of {} to keep at most {} cache entries", | ||
| evicted.display(), | ||
| self.document_caches.max() | ||
| ); | ||
| doc.cache().clear(); | ||
| } | ||
| } |
Contributor
There was a problem hiding this comment.
Could we also charge the document immediately after parser.find(..., ExecuteStatementMapper) in execute_statement? The lookup caches preceding statements, but this workspace method never records their usage. The standard LSP command normally catches up via update_all_diagnostics; direct workspace callers and errors that return before that refresh do not. Charging here would enforce the budget independently of the caller and cover the early-return paths.
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #1005. Part of the rebuild described in #997 (point 5: one memory budget). Replaces #951, #953's budget, #956 and #957.
Each open document keeps the parse result and tree of every statement (#913), its analysis (#1004), and what the walk resolved (#1005). Nothing bounded that across documents: a statement's parse tree takes about 10 KB, so 20 open migration files of 5,000 statements kept 2.7 GB until they were closed. #951 and #953 bounded the document caches and the walk cache with two budgets of their own; here there is one.
What changed
document_cache/budget.rs:CacheBudget<K>(from perf(workspace): bound the memory of the document caches #951).record(key, usage)updates the key's usage and last use, and returns the other keys to evict, least recently used first, until the total is within its maximum. The key just used is never evicted, so a single file larger than the budget stays cached while it is used.remove(key)stops tracking it.DocumentCache::usagecounts what a document keeps in one unit: its parse results, trees and resolved statements, andCHECKPOINT_ENTRIESper walk checkpoint (a checkpoint copies the maps of the objects the file changed before it).DocumentCache::cleardrops all of it.charge_document_caches), and clears the caches it is told to evict.DOCUMENT_CACHE_ENTRIES_MAX = 20_000entries, about 200 MB at the measured 10 KB per entry: enough to keep a few files of several thousand statements fully cached. Eviction is logged at debug level, and an evicted document recomputes on its next request, with the same results.check,format) closes each file after it is processed, so it holds at most one file per worker.#[doc(hidden)] WorkspaceServer::document_cache_statsreports what a document's cache holds, for the memory tests. Counting doesn't fill the cache.Tests
evicts_the_least_recently_used_keys_first(unit): LRU order, the key in use is never evicted,remove.document_caches_stay_within_the_budget: with a small budget, using a second document drops the caches (parse results and analysis) of the first; using the first again recomputes them with the same diagnostics; closing removes it from the budget.tests/memory_bounds.rs(from perf(workspace): bound the memory of the document caches #951) counts every allocation with a global allocator. It opens N migration files, runs diagnostics, hover, inlay hints and code actions on each, closes them, then runs the CLI path (open, diagnostics, close) on the same files. It asserts that what is retained with every document open stays within what the budget can keep plus the text of the others, that the peak stays within that plus one document, that closing every document leaves less than 4 MiB, and that the CLI's peak is at most twice that of one document.MEMORY_DOCUMENTSandMEMORY_GROUPSchange the workload.tests/edit_churn.rs(from test(workspace): the document caches follow a long editing session #956 and test(workspace): check that editing a document doesn't grow the walk cache #957) applies 2,000 seeded random edits to a 1,000-statement document in 500 changes, with hover and inlay hints every 5 changes and diagnostics, code actions and full semantic tokens every 50. After every change the walk keeps at most 4 checkpoints and no more resolved statements than the document has; at the end every statement has exactly one parse result, tree and set of references, and the memory retained after the edits is at most 1.5 times that after the first pass.pgls_workspacepasses, and clippy on the whole workspace.Debug build, the CI defaults:
memory_bounds, 20 documents × 1,000 statements: one documentedit_churn, 1,000 statements, 2,000 edits: retained after the first pass / after the editsThe tests take about 30 and 40 seconds in a debug build.
Release build, 20 documents × 5,000 statements (9.6 MiB of SQL), with the whole stack up to #1008. "Before" is #951's measurement of the same test without a budget:
Not covered