Repository navigation
[Vsintegration] Fix lost assembly references and project cleanup in the code model - #2127
Merged
RobertvanderHulst merged 9 commits intoOct 27, 2026
Merged
Conversation
While XSolution.Close() closed the database on the UI thread, background threads (CPS design-time builds, the ModelWalker) kept using it, and several threads read the same assemblies at the same time. In the logs this showed up as "No connection associated with this command", "Database is not open" and NullReferenceExceptions in AssemblyReader.AddType, OnTargetDesignTimeBuildChanged and FindType. - XDatabase: all access to the in memory connection is serialized by a fixed lock object instead of oConn, which CloseDatabase sets to NULL. Every method checks IsDbOpen again once it holds the lock, and CloseDatabase closes the connection under that lock. The backup locks only around BackupDatabase as before; the VACUUM stays outside. - XAssembly.Read() runs under a lock per assembly and does not read again when another thread has just read the unchanged file. When the database is closed, the file time and size are kept in memory, so type lookups do not read the assembly over and over. - SystemTypeController.LoadAssembly: one lock per file name. Threads that asked for the same assembly at the same time each created (and read) their own XAssembly; the logs showed 30 reads of one file. - XSharpProjectAdapter: no reference resolution, file changes or new target models while the solution closes or after it has closed. CPS keeps delivering design-time builds until the projects are unloaded. - XProject.RootNamespace no longer fails after Close() cleared the project node while a background parse still resolves types. Measured in the VS 2022 experimental instance with RadixWfSDK.slnx, closed 60 s after loading, with a new database and devenv pinned to the E-cores (the race only shows while VS is still busy): 75 errors between "Closing solution" and "Shutdown VS" before, 0 after; the close took 16.2 s before and 2.6 s after. Assembly reads during the load went from 656-3354 to 566-595. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SaveProjectAssemblyReferences deleted every assembly that no project
referenced yet ("delete from Assemblies where id not in (select
IdAssembly from AssembliesPerProject)"). The projects resolve their
references on many threads, so the first project that saved its
references deleted assemblies other projects had just read but not
linked yet, together with their types (cascade). Every later save of
every project then failed with "FOREIGN KEY constraint failed" on those
rows, so no project stored its assembly references, and the next load
inserted, read and deleted the same assemblies again. Measured with
RadixWfSDK.slnx: one cleanup deleted 8 assemblies, and the saves of 93
of 97 projects failed on them.
- The cleanup of unreferenced assemblies now runs in DeleteOrphanFiles
when the database is opened, before any XAssembly has its Id.
- SystemTypeController.LoadAssembly created and read a second XAssembly
for mscorlib.dll on every call (once per project); it now uses the
cached instance.
- ModelWalker: after Stop() the walk of the current project no longer
finishes as if all files were walked (FileWalkCompleted,
ProjectWalkComplete, type resolution).
Measured in the VS 2022 experimental instance (six runs: settled, new
database, devenv on the E-cores): 0 errors in the whole log, before
38-164 per load. Every assembly is read once per load (469 for 469
files, mscorlib 98 times before), and a load with an up to date database
writes no assembly data any more.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e walked walkOneFile also skips files while the walker is suspended, and quick info, completion, signature help and peek suspend it while a walk runs. The project was then still finished as walked (FileWalkCompleted, ProjectWalkComplete, type resolution), and the skipped files were not walked again. The MustAbort check of the previous commit only covered Stop(). MustAbort is global state: when the next solution starts a new walker thread, _state is Running again, so an old thread that was still busy with its files after Stop() finished its project against the new database. It also set the static _currentProject to NULL, so the new thread could skip all files of its project and finish it as walked. - Stop() increments a generation. Each walker thread gets the generation it was started for and stops when it changed (IsAborted). - The project and the count of walked files are per walk (ProjectWalk) instead of the static _currentProject. - A project with skipped files is queued again, unless the walk was aborted. The enqueue for type resolution checks the generation under the lock that Stop() takes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…the solution closes DeleteProject is commented out (4242326): AfterOpen() called it for every project in the database that was not registered yet, but the code models of CPS projects (one per TargetFramework) are created after AfterOpen(), so it deleted their files and types on every load. Since then nothing deletes projects: a project removed from the solution or a TargetFramework that is no longer used stays in the database with its files, types and assembly references, and its assemblies are never cleaned up either. - XDatabase.Read(XProject) remembers the Ids of the projects read since the database was opened. - XSolution.Close() deletes all other projects (cascade) before the database is saved, only when AfterOpen() ran for the solution: a solution closed while it is still loading has projects that were not read yet. The files and assemblies they leave behind are deleted by DeleteOrphanFiles when the database is opened again. A project that was unloaded during the whole session is deleted as well; it is parsed again when it is loaded. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… pending The CPS adapter creates the code models of the TargetFrameworks of a cross targeting project asynchronously, after AfterOpen(): only when the configuration group has arrived and the configured project is loaded. A solution that was closed before then deleted the database rows of those models with their files and types (DeleteProjectsNotInSession), and they were parsed again on the next load. - The adapter knows the TargetFrameworks that get a model (from the project file in LoadAsync, then from the configuration group) and reports to XSolution.SetModelsPending whether one of them has no model yet. UnloadAsync clears it. - XSolution.Close() deletes the projects only when no CPS project has pending models, and logs why it skipped the cleanup. A target whose configured project can not be loaded stays pending, so no projects are deleted in that session. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Left over from a14f11b (one code model per TargetFramework): - UpdateTargetSubscriptionsAsync added the model of a dropped target twice to the models to close. The second Add was not under the if that excludes the primary model, so a model was closed twice, and the primary model could be closed although "the primary model is closed in UnloadAsync, never here". - LoadAsync subscribed to ProjectWalkComplete and FileWalkComplete of the primary model, which CreateModel had already done, so its handlers (comment tasks, file walk) ran twice. It also set the model twice. - A nested lock (modelGate) inside lock (modelGate), with an assignment of keep that was overwritten right away. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… remove dead code in AfterOpen - WalkSource left the status bar animation on when Stop() was called after the last file was walked. - AfterOpen collected the project names from the database only to call DeleteProject, which no longer deletes anything: projects that are no longer part of the solution are deleted when it closes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-cleanup # Conflicts: # src/VisualStudio/ProjectSystemCPS/LanguageService/XSharpProjectAdapter.cs # src/VisualStudio/XSharpCodeModelXs/Database/XDatabase.prg
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.
Follow-up to #2125. This PR fixes the FOREIGN KEY failures that were still open there, along with several walker and cleanup problems in the code model that showed up while investigating them.
Assembly references lost in the database
SaveProjectAssemblyReferencesdeleted every assembly that no project referenced yet (delete from Assemblies where id not in (select IdAssembly from AssembliesPerProject)). Projects resolve their references on many threads, so the first project to save deleted assemblies that other projects had just read but not linked yet, together with their types (cascade).FOREIGN KEY constraint failedon those rows, so no project stored its assembly references.Changes:
DeleteOrphanFileswhen the database is opened, before anyXAssemblyhas its Id.XAssemblyfor mscorlib.dll on every call (once per project). It now uses the cached instance.ModelWalker
Stop()(the solution closes) orSuspend()(completion, quick info and so on) skipped files,FileWalkCompleted,ProjectWalkCompleteand type resolution no longer run for it.ProjectWalk) instead of static fields, and every walker thread checks its generation. A thread that still finishes files afterStop()can no longer change the walk of the next solution.Projects that are no longer part of the solution
AfterOpendeleted every database project that was not loaded yet. CPS projects create one code model per TargetFramework asynchronously, afterAfterOpen, so their files and types were deleted and parsed again on every load. Projects that noXProjecthas read in this session are now deleted when the solution closes, before the database is saved.XSolution.SetModelsPending, set byXSharpProjectAdapter).Test
VS 2022 experimental instance, RadixWfSDK.slnx:
[ERR], 0 FOREIGN KEY failures, 0 projects deleted at close.AfterOpen). It builds.🤖 Generated with Claude Code