-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Avoid lock when setting project entry point #11987
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Avoid lock when setting project entry point #11987
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR refactors the build submission flow to reduce lock contention and implements an atomic setter for telemetry entry points.
- Migrate
_buildSubmissions
fromDictionary
toConcurrentDictionary
and update all related operations (add, remove, count, enumeration). - Remove the
lock
inPendBuildRequest
by using atomic operations (Interlocked
,Thread.MemoryBarrier
) andTryAdd
. - Extract
ProjectPath
/BuildTarget
assignment intoBuildTelemetry.SetProjectEntryPoint
withInterlocked.CompareExchange
and update tests accordingly.
Reviewed Changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
File | Description |
---|---|
src/Framework/Telemetry/BuildTelemetry.cs | Added _projectEntryPointSet flag, made ProjectPath /BuildTarget setters private, and introduced SetProjectEntryPoint . |
src/Build/BackEnd/BuildManager/BuildManager.cs | Replaced submission tracking with ConcurrentDictionary , removed the lock in PendBuildRequest , updated loops and ID generation. |
src/Build.UnitTests/BackEnd/KnownTelemetry_Tests.cs | Updated telemetry unit test to call SetProjectEntryPoint instead of setting properties directly. |
Comments suppressed due to low confidence (2)
src/Framework/Telemetry/BuildTelemetry.cs:234
- The comment refers to an
entryPointSet
field but the actual backing field is named_projectEntryPointSet
. Updating the comment to match the field name will improve clarity.
// this check is to ensure that we only set the entry point once. The entryPointSet field is set to 0
src/Framework/Telemetry/BuildTelemetry.cs:232
- Introduce unit tests to verify that calling
SetProjectEntryPoint
multiple times does not override the initially setProjectPath
andBuildTarget
, ensuring idempotent behavior is covered.
public void SetProjectEntryPoint(IEnumerable<string> entryProjectsFullPath, IEnumerable<string> targetNames)
@@ -2172,7 +2176,7 @@ private void ShutdownConnectedNodes(bool abort) | |||
/// </summary> | |||
private int GetNextSubmissionId() | |||
{ | |||
return _nextBuildSubmissionId++; | |||
return Interlocked.Increment(ref _nextBuildSubmissionId); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using Interlocked.Increment changes the first submission ID from 0 to 1, altering existing behavior. Consider initializing _nextBuildSubmissionId
to -1 or adjusting the logic to preserve the original ID sequence starting at 0.
Copilot uses AI. Check for mistakes.
_buildTelemetry?.SetProjectEntryPoint(requestData.EntryProjectsFullPath, requestData.TargetNames); | ||
|
||
// SubmissionId should always be unique since GetNextSubmissionId() uses Interlocked.Increment. | ||
_buildSubmissions.TryAdd(newSubmission.SubmissionId, newSubmission); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
_buildSubmissions.TryAdd(newSubmission.SubmissionId, newSubmission); | |
bool added = _buildSubmissions.TryAdd(newSubmission.SubmissionId, newSubmission); | |
Debug.Assert(added == true, "We expect adding a new submission to always succeed since it should have received a unique ID"); |
?
@@ -1009,7 +1008,12 @@ public void EndBuild() | |||
lock (_syncLock) | |||
{ | |||
// If there are any submissions which never started, remove them now. | |||
var submissionsToCheck = new List<BuildSubmissionBase>(_buildSubmissions.Values); | |||
var submissionsToCheck = new List<BuildSubmissionBase>(_buildSubmissions.Count); | |||
foreach (KeyValuePair<int, BuildSubmissionBase> kvp in _buildSubmissions) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure I understand this change--if we want a local iterable don't we want ConcurrentDictionary<>.Values
?
Also is there value in building the list then iterating over it, versus combining the foreach
es?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The motivation for creating the list is that CheckSubmissionCompletenessAndRemove()
mutates _buildSubmissions
so we need a copy.
private void CheckSubmissionCompletenessAndRemove(BuildSubmissionBase submission)
{
lock (_syncLock)
{
// If the submission has completed or never started, remove it.
if (submission.IsCompleted || !submission.IsStarted)
{
_overallBuildSuccess &= (submission.BuildResultBase?.OverallResult == BuildResultCode.Success);
_buildSubmissions.TryRemove(submission.SubmissionId, out _);
// Clear all cached SDKs for the submission
SdkResolverService.ClearCache(submission.SubmissionId);
}
CheckAllSubmissionsComplete(submission.BuildRequestDataBase.Flags);
}
}
The switch for enumerating the KVP vs just the Values collection is because of the difference between how ConcurrentDictionary
and Dictionary
expose the Values property. Using Dictionary
is nice since the concrete type exposes struct-based enumerators for Keys, Values, and the KVP. In contrast, ConcurrentDictionary
creates a list of all the values and returns that:
public ICollection<TValue> Values
{
[__DynamicallyInvokable]
get
{
return GetValues();
}
}
private ReadOnlyCollection<TValue> GetValues()
{
int locksAcquired = 0;
try
{
AcquireAllLocks(ref locksAcquired);
int countInternal = GetCountInternal();
if (countInternal < 0)
{
throw new OutOfMemoryException();
}
List<TValue> list = new List<TValue>(countInternal);
for (int i = 0; i < m_tables.m_buckets.Length; i++)
{
for (Node node = m_tables.m_buckets[i]; node != null; node = node.m_next)
{
list.Add(node.m_value);
}
}
return new ReadOnlyCollection<TValue>(list);
}
finally
{
ReleaseLocks(0, locksAcquired);
}
}
Just enumerating the ConcurrentDictionary
enumerates the KVPs via the yield return pattern
public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator()
{
Node[] buckets = m_tables.m_buckets;
for (int i = 0; i < buckets.Length; i++)
{
for (Node current = Volatile.Read(ref buckets[i]); current != null; current = current.m_next)
{
yield return new KeyValuePair<TKey, TValue>(current.m_key, current.m_value);
}
}
}
We still get an allocation from the generated state machine for the enumerator, but it will be better than cloning all the values unless we consistently put almost nothing in the dictionary.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, still not seeing it. Here you walk all the KVPs and add their value to a list, then walk the list doing work. If you called .Values
, wouldn't that GetValues()
do almost exactly that same list construction?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, I think it just clicked. You're proposing using the list returned from Values
directly since it's a copy rather than manually making our own copy? If so, I agree with what you're suggesting. I can update this to just operate on the Values
collection and add a comment explaining why it's safe to do so even with CheckSubmissionCompletenessAndRemove()
mutating the original dictionary.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, that's what I was thinking for this one.
…rndt/PendBuildRequestContention
Context
There's a good amount of contention inside of
BuildManager.PendBuildRequest()
coming from the lock in the body:There's a small amount of CPU time caused by hitting the lock too.
Looks like this can be switched over to use some low-lock patterns to mitigate this. The results after the changes show the contention is essentially eliminated
Changes Made
Testing
Notes