Skip to content

Remove weak references in skeleton reference cache.#61608

Merged
CyrusNajmabadi merged 12 commits intodotnet:mainfrom
CyrusNajmabadi:solutionSync
Jun 1, 2022
Merged

Remove weak references in skeleton reference cache.#61608
CyrusNajmabadi merged 12 commits intodotnet:mainfrom
CyrusNajmabadi:solutionSync

Conversation

@CyrusNajmabadi
Copy link
Copy Markdown
Contributor

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.

@ghost ghost added the Area-IDE label May 31, 2022
/// calls can early exit.
/// The actual assembly metadata produced from the data pointed to in <see cref="_storage"/>.
/// </summary>
private readonly AsyncLazy<AssemblyMetadata?> _metadata;
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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();
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as before, but no longer weak. As per the OP of this PR, having htis be weak seems like an antipattern.

Copy link
Copy Markdown
Contributor

@heejaechang heejaechang Jun 1, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@CyrusNajmabadi CyrusNajmabadi marked this pull request as ready for review May 31, 2022 21:39
@CyrusNajmabadi CyrusNajmabadi requested a review from a team as a code owner May 31, 2022 21:39
@tmat
Copy link
Copy Markdown
Member

tmat commented May 31, 2022

                // it would be great if compiler can be little bit smarter on how it deals with stream.

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);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s_lifetime

Since we now hold on the metadata in AsyncLazy, do we actually need this?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 :)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, so alternatively we could implement a subtype of PortableExecutableReference, return an instance of it and store the memory object to it.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 :)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@CyrusNajmabadi
Copy link
Copy Markdown
Contributor Author

Do you know what this comment means? What should the compiler do better?

I don't know. I think @heejaechang may have written the comment. He might know :)

@CyrusNajmabadi CyrusNajmabadi requested review from sharwell and tmat May 31, 2022 23:22
public void StopListening(Workspace workspace)
{
if (!(workspace is VisualStudioWorkspace) || IVsShellExtensions.IsInCommandLineMode(_threadingContext.JoinableTaskFactory))
if (workspace is not VisualStudioWorkspace || IVsShellExtensions.IsInCommandLineMode(_threadingContext.JoinableTaskFactory))
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Change is fine, not sure if you meant it to be in this PR though?

@heejaechang
Copy link
Copy Markdown
Contributor

heejaechang commented Jun 1, 2022

Hi there :)

Do you know what this comment means? What should the compiler do better?

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.

@CyrusNajmabadi CyrusNajmabadi enabled auto-merge June 1, 2022 15:12
@CyrusNajmabadi CyrusNajmabadi merged commit c025a20 into dotnet:main Jun 1, 2022
@ghost ghost added this to the Next milestone Jun 1, 2022
@CyrusNajmabadi CyrusNajmabadi deleted the solutionSync branch June 1, 2022 16:48
@RikkiGibson RikkiGibson modified the milestones: Next, 17.3 P3 Jun 28, 2022
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.

6 participants