Skip to content

Conversation

@ToddGrun
Copy link
Contributor

@ToddGrun ToddGrun commented Apr 23, 2025

Solution/Project classes keep several immutable dictionaries for mapping (Document/Project)Id to various data. ImmutableDictionaries are a fine (but slow) data structure when data between versions is to be shared, but none of the values in these ImmutableDictionaries persist between solution/project transformations. In fact, the only operation performed on these dictionaries is GetOrAdd calls. As immutable dictionaries are quite poor performing wrt lookups/adds, I thought I would try out two approaches to see if they showed improvements:

  1. Switch to ConcurrentDictionary (see *** WIP: Switch Project data structures from ImmutableDictionary => ConcurrentDictionary #78285).
  2. Switch to simple Dictionaries guarded by locks (this PR)

I created test insertions for each of these PRs to gather speedometer numbers, particularly during the solution load of the c# editing speedometer test. The measurements below are from GC Heap Allocs and CPU samples for those tests under (Solution/Project).GetDocument calls

  1. ConcurrentDictionary change test insertion: https://dev.azure.com/devdiv/DevDiv/_git/VS/pullrequest/631277
  2. Dictionary/lock test insertion: https://dev.azure.com/devdiv/DevDiv/_git/VS/pullrequest/631278

*** Baseline ***
Alloc: 120 MB (1.0%)
CPU: 560 ms (0.3%)

*** Concurrent Dictionary ***
Alloc: 59 MB
CPU: 258 ms (0.1%)

*** Dictionary with lock ***
Alloc: 28 MB
CPU: 172 ms (< 0.1%)

So, it looks like the best performing of the bunch is this PR, which uses simple dictionaries and locks, thus why I'm elevating this PR out of draft mode. The ConcurrentDictionary PR is interesting as the code changes are simple and do give a nice perf improvement.

…ictionary and lock

Will add more information if speedometer run provides compelling results
@ghost ghost added Area-IDE untriaged Issues and PRs which have not yet been triaged by a lead labels Apr 23, 2025
@ToddGrun ToddGrun changed the title *** WIP: Switch Project data structures from ImmutableDictionary => Dictionary and lock Switch Project data structures from ImmutableDictionary => Dictionary and lock Apr 25, 2025
@ToddGrun ToddGrun marked this pull request as ready for review April 25, 2025 00:03
@ToddGrun ToddGrun requested a review from a team as a code owner April 25, 2025 00:03
private ImmutableDictionary<DocumentId, AnalyzerConfigDocument?> _idToAnalyzerConfigDocumentMap = ImmutableDictionary<DocumentId, AnalyzerConfigDocument?>.Empty;
private readonly object _gate = new();

// Only access these dictionaries when holding _gate
Copy link
Member

Choose a reason for hiding this comment

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

alternatively, they could be their own locks. but i don't care that much. this would also enable concurrent access to all of them, while each one was threadsafe. up to you.

Copy link
Member

Choose a reason for hiding this comment

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

oh. nevermind. you use null. ignore this.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Went ahead and switched the access of the dictionaries to be protected by using themselves as locks. Don't love it, but it does give better lock granularity.


// Quick check first: if we already have created a SourceGeneratedDocument wrapper, we're good
if (_idToSourceGeneratedDocumentMap.TryGetValue(documentId, out var sourceGeneratedDocument))
if (_idToSourceGeneratedDocumentMap != null && _idToSourceGeneratedDocumentMap.TryGetValue(documentId, out var sourceGeneratedDocument))
Copy link
Member

Choose a reason for hiding this comment

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

accessing out of lock. not safe.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Bah, this one was the ugly stepchild, and I only looked at the GetOrAdd usages, but yes, the TryGetValue calls need to be protected too. Added methods for both GetOrAdd and TryGetValue that encapsulate the locking.

_idToAnalyzerConfigDocumentMap ??= [];
return _idToAnalyzerConfigDocumentMap.GetOrAdd(documentId, s_tryCreateAnalyzerConfigDocumentFunction, this);
}
}
Copy link
Member

Choose a reason for hiding this comment

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

small nit. consider extracting helper for this pattern. we often have triplicate methods for docs/analyzerdocs/additionaldocs. i lke it when they all call into a single helper.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Extracted both GetOrAdd and TryGetValue calls into their own methods that can be called for any of the dictionaries.

// Values for all these are created on demand.
private ImmutableDictionary<ProjectId, Project> _projectIdToProjectMap;
// Values for all these are created on demand. Only access when holding _gate.
private readonly Dictionary<ProjectId, Project> _projectIdToProjectMap = [];
Copy link
Member

Choose a reason for hiding this comment

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

since you do initialize this, consider having hte dict be its own lock.

Copy link
Member

Choose a reason for hiding this comment

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

are you auditing all usages of _projectIdToProjectMap to make sure it is locked?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, I did check all five dictionaries converted, just didn't notice the TryGetValue calls on _idToSourceGeneratedDocumentMap

…Todd!

2) Create common shared methods for each of the dictionaries to get/add items that handles the lock.

3) Don't love it, but made the dictionaries be their own lock for accessing them.
@ToddGrun ToddGrun merged commit 567495e into dotnet:main Apr 25, 2025
25 checks passed
@dotnet-policy-service dotnet-policy-service bot added this to the Next milestone Apr 25, 2025
@RikkiGibson RikkiGibson modified the milestones: Next, 18.0 P1 Aug 19, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Area-IDE untriaged Issues and PRs which have not yet been triaged by a lead VSCode

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants