Repository navigation
fix: slow saves from runaway flow analysis and full workspace re-checks - #24
Merged
Merged
Conversation
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.
Changes
After a few updates, saving Lua files in VS Code started to hang for me on a large server workspace (~800 Lua files, 146 resources). VS Code waits for
codeActionsOnSave(I havesource.fixAll/source.organizeImportsenabled globally), and since the server is single-threaded the request sat in the queue behind work the server was already doing. There were two causes:1. Flow analysis blew up on locals that walk nested tables (
narrow.rs)Code that reuses one local to step through nested tables, like reading a config or walking a tree:
Each of these assignments copied every alias the local already had, extended by the new key, so the alias list doubled with every line. On top of that, the
containscheck before each push was linear. A few lines of this is harmless, but the cost grows as 2^n, and one ~1,100-line file that does it a lot (with locals copied between functions) was enough to keepFlow::ofbusy for minutes, and the startup scan never finished.tables_ofnow drops tables read throughMAX_KEYSkeys or more. Every consumer requireskeys.len() > prefix.len(), and tracked fields have at mostMAX_KEYSkeys, so those entries could never match anything. The results stay the same.Aliasesnow holds aFxHashSetinstead of aVec. The consumers only useany, so order doesn't matter. Locals copied from each other still pick up thousands of entries, and this removes the quadratic dedup.2. Every save re-checked the whole workspace (
server.rs)After a save, VS Code also sends
workspace/didChangeWatchedFiles(CHANGED) for the file it just wrote.watched_files_changedsetworkspace_stale = truefor any change, so each save re-linted every closed file in the workspace.didSavealready marks their resource as stale.refresh_readersreached, and the resources of files that now declare something differently. That is the same scope a save uses..cfgfile, a manifest, or a lint config changes, or when imports are relinked.I tracked this down and wrote the fix with help from generative AI (Claude Code, Opus 5.5). That included profiling, bisecting the slow file, and drafting the patch. I reviewed the changes and checked the results myself.
Benchmarks
Release build on Windows, same private workspace for both: ~800 Lua files, 146 resources.
qbx-lua-ls --indexon the whole workspaceindexed … in)Flow::of, first 740 lines of the heaviest fileFlow::of, the same file in full (~1,100 lines)codeActionsOnSavereply (fixAll + organizeImports) after an editThe
Flow::ofcost grew exponentially with the number oft = t.keylines. On the 740-line prefix, line 739 took 343 ms and line 740 took about 20 s. For the full file, the first change alone (dropping the long tables) brought it to ~940 ms, and the hash set brought it to 78 ms.For the save numbers I used a small LSP client that opens a large file, sends a few edits, then sends the two
codeActionrequests VS Code makes on save, followed bydidSaveand the watched-file event, three times in a row. With 1.1.0 the client never got past the startup scan, so I have no per-save number for the old build.Checks
cargo test --workspace --locked --release: 593 passed, 0 failedcargo fmt --all --checkandcargo clippy --workspace --all-targets --locked -- -D warnings: cleanworkspace_refresh::locals_assigned_from_their_own_fields_over_and_over_are_indexed_quickly: 40 chainedt = t.keyNassignments. It hangs onmainand takes a few ms with this change.lsp::saving_an_open_document_leaves_the_other_resources_unchecked: a save plus the watched-file event no longer re-publishes another resource's closed file, and a real on-disk change to that file still re-checks it.npm run server+vsce package --target win32-x64) and ran the LSP client above against it.