Remove weak references in skeleton reference cache.#61608
Remove weak references in skeleton reference cache.#61608CyrusNajmabadi merged 12 commits intodotnet:mainfrom
Conversation
| /// calls can early exit. | ||
| /// The actual assembly metadata produced from the data pointed to in <see cref="_storage"/>. | ||
| /// </summary> | ||
| private readonly AsyncLazy<AssemblyMetadata?> _metadata; |
There was a problem hiding this comment.
this is a change from before. previously we might create several instances of AssemblyMetadta if the same compilation was asked for multiple MetadataReferences to it (with different MetadataReferenceProperties). now, you'll get the same AssemblyMetadata that will be reused in all those cases.
There was a problem hiding this comment.
This supports the compiler docs which explicitly state:
/// <see cref="Metadata"/> objects may cache information decoded from the PE image.
/// Reusing <see cref="Metadata"/> instances across metadata references will result in better performance.
| /// This instance should be locked when being read/written. | ||
| /// </remarks> | ||
| private readonly Dictionary<MetadataReferenceProperties, WeakReference<MetadataReference>?> _metadataReferences = new(); | ||
| private readonly Dictionary<MetadataReferenceProperties, AsyncLazy<MetadataReference?>> _metadataReferences = new(); |
There was a problem hiding this comment.
same as before, but no longer weak. As per the OP of this PR, having htis be weak seems like an antipattern.
There was a problem hiding this comment.
hmmm.. if weak reference is going away, I think that means the primaryWorkspace in OOP is having problem (not stay alive) or ftemporaryWorkspace forked from primaryWorkspace is not getting skeleton assembly as expected and re-creating them. or property instance is not reused properly?
but I guess with 64bits, holding some skeleton assemblies in memory a bit longer won't matter much now.
src/Workspaces/Core/Portable/Workspace/Solution/SolutionState.SkeletonReferenceCache.cs
Outdated
Show resolved
Hide resolved
src/Workspaces/Core/Portable/Workspace/Solution/SolutionState.SkeletonReferenceCache.cs
Outdated
Show resolved
Hide resolved
Do you know what this comment means? What should the compiler do better? Refers to: src/Workspaces/Core/Portable/Workspace/Solution/SolutionState.SkeletonReferenceCache.cs:311 in de7b4c5. [](commit_id = de7b4c5, deletion_comment = False) |
| // Tie lifetime of stream to metadata we created. It is important to tie this to the Metadata and not the | ||
| // metadata reference, as PE symbols hold onto just the Metadata. We can use Add here since we created | ||
| // a brand new object in AssemblyMetadata.Create above. | ||
| s_lifetime.Add(metadata, supportNativeMemory); |
There was a problem hiding this comment.
We do yes, and it's vital (fortunately tests will crash). The reason for this is that metadata-references are handed out and can effectively live longer than our own internal data structures. One way to imagine this is simply that we handle out compilations through our Project API, but the user then lets go of the Solution instance (or it moves forward on the workspace). All the existing snapshots in the Solution/Project/CompolationTracker snapshot get GC'ed and explosions happen.
Effectively, we're saying as long as anything is referencing this metadata, this stream cannot be removed.
--
Note: i learned this from actually trying to remove it and discovering that yes it does blow up :)
There was a problem hiding this comment.
I see, so alternatively we could implement a subtype of PortableExecutableReference, return an instance of it and store the memory object to it.
There was a problem hiding this comment.
I wonder if it would be best to add ModuleMetadata.CreateFromImage() overload that takes an object memoryOwner. That way whenever you create a pointer based module metadata you can just pass in the object that keeps it alive and not worry about it.
There was a problem hiding this comment.
I see, so alternatively we could implement a subtype of PortableExecutableReference, return an instance of it and store the memory object to it.
Yes. That would work. But there's already a subclass being returned. Slightly worried about having to preserve semantics with that. However, i am very open to this as i hate CWTs and would love to get rid of that, so let me try that.
There was a problem hiding this comment.
I wonder if it would be best to add ModuleMetadata.CreateFromImage() overload that takes an object memoryOwner. That way whenever you create a pointer based module metadata you can just pass in the object that keeps it alive and not worry about it.
That would be a way to do it :)
There was a problem hiding this comment.
ok. i started down teh compiler side, and it was a lot of changes to the public API. I'll do that in a followup. FOr now though i made a custom subclass where we can hold onto the memory ourselves.
I don't know. I think @heejaechang may have written the comment. He might know :) |
src/Workspaces/Core/Portable/Workspace/Solution/SolutionState.SkeletonReferenceCache.cs
Outdated
Show resolved
Hide resolved
src/Workspaces/Core/Portable/Workspace/Solution/SolutionState.SkeletonReferenceCache.cs
Outdated
Show resolved
Hide resolved
…SkeletonReferenceCache.cs
| public void StopListening(Workspace workspace) | ||
| { | ||
| if (!(workspace is VisualStudioWorkspace) || IVsShellExtensions.IsInCommandLineMode(_threadingContext.JoinableTaskFactory)) | ||
| if (workspace is not VisualStudioWorkspace || IVsShellExtensions.IsInCommandLineMode(_threadingContext.JoinableTaskFactory)) |
There was a problem hiding this comment.
Change is fine, not sure if you meant it to be in this PR though?
|
Hi there :)
It refers to comments right above - "if we give stream, compiler will internally copy it to native memory again", it means it would be nice if compiler (by compiler, roslyn's compiler code) know when to copy or reuse given stream so we don't need to have this in IDE side. and all we need to do is just AssemblyMetadata.CreateFromStream and compiler side does everything in the most efficient way and deal with lifetime themselves in case of re-using native memory. |
Traces taken indicate we're consume lots of resources generating skeleton references in OOP. Auditing the code i found the use of weak-references to hold onto skeletons highly suspect. The presumption should be that if we've computed the skeleton for a compilation because a downstream project needs it, then we'll almost certainly continue to need that skeleton in the future as the downstream project changes. We'd only want to actually let it get cleaned up when the compilatino-tracker owning hte skeleton actually goes away.