-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Several allocation fixes in Scheduler.cs #11802
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?
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 pull request addresses allocation fixes and refactors parts of the Scheduler to improve performance and correctness during request scheduling.
- Preallocates the HashSet for idle nodes based on available capacity.
- Adjusts unscheduled request list initialization and updates loop ordering in FIFO assignment.
- Refactors and modernizes static local method usage for configuration and logging.
@@ -1246,7 +1247,7 @@ private void AssignUnscheduledRequestsWithMaxWaitingRequests2(List<ScheduleRespo | |||
private void AssignUnscheduledRequestsFIFO(List<ScheduleResponse> responses, HashSet<int> idleNodes) | |||
{ | |||
// Assign requests on a first-come/first-serve basis | |||
foreach (int nodeId in idleNodes) | |||
foreach (SchedulableRequest unscheduledRequest in _schedulingData.UnscheduledRequestsWhichCanBeScheduled) |
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 loop order in AssignUnscheduledRequestsFIFO has been reversed, which may affect the intended FIFO scheduling behavior. Please verify that iterating unscheduled requests first and then idle nodes produces the expected request assignment order.
foreach (SchedulableRequest unscheduledRequest in _schedulingData.UnscheduledRequestsWhichCanBeScheduled) | |
foreach (int nodeId in idleNodes) |
Copilot uses AI. Check for mistakes.
@@ -991,7 +991,8 @@ private void AssignUnscheduledRequestsToInProcNode(List<ScheduleResponse> respon | |||
{ | |||
if (idleNodes.Contains(InProcNodeId)) | |||
{ | |||
List<SchedulableRequest> unscheduledRequests = new List<SchedulableRequest>(_schedulingData.UnscheduledRequestsWhichCanBeScheduled); | |||
List<SchedulableRequest> unscheduledRequests = new List<SchedulableRequest>(_schedulingData.UnscheduledRequestsCount); |
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 _schedulingData.UnscheduledRequestsCount as the list capacity assumes it matches the actual number of unscheduled requests. Please ensure that this value is always in sync with _schedulingData.UnscheduledRequestsWhichCanBeScheduled to avoid potential resizing during list population.
List<SchedulableRequest> unscheduledRequests = new List<SchedulableRequest>(_schedulingData.UnscheduledRequestsCount); | |
List<SchedulableRequest> unscheduledRequests = new List<SchedulableRequest>(_schedulingData.UnscheduledRequestsWhichCanBeScheduled.Count); |
Copilot uses AI. Check for mistakes.
This appears to have killed all the pipelines in a weird way. |
|
||
static Action<ILoggingService> GetLoggingServiceAction(IConfigCache configCache, BuildRequest request, SchedulingData schedulingData) | ||
{ | ||
// retrieving the configs is not quite free, so avoid computing them eagerly |
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.
Can you elaborate on this? Doesn't the next line realize the configs?
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.
Oh this is copy/pasted from the original. Not sure I see the benefit of this extraction, does it cause some subtle allocation thing?
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 goal here is to avoid the allocation of a closure object, and an unfortunate consequence is that if there's a capture anywhere in the body of the method, the closure object is created. This happens regardless of if the code that would use the closure is called. For example this:
public static Action Test(int input)
{
Action action;
if (input < int.MinValue)
{
action = () => Console.WriteLine(input);
}
else
{
action = () => { };
}
return action;
}
Compiles to this:
public static Action Test(int input)
{
<>c__DisplayClass1_0 <>c__DisplayClass1_ = new <>c__DisplayClass1_0();
<>c__DisplayClass1_.input = input;
if (<>c__DisplayClass1_.input < int.MinValue)
{
return <>c__DisplayClass1_.<Test>b__0;
}
return <>c.<>9__1_1 ?? (<>c.<>9__1_1 = <>c.<>9.<Test>b__1_1);
}
c__DisplayClass1_0
is unconditionally allocated.
By pulling the offending code into a local function, we avoid the unconditional allocation:
public static Action Test(int input)
{
Action action;
if (input < int.MinValue)
{
action = WriteConsoleAction();
}
else
{
action = () => { };
}
return action;
Action WriteConsoleAction()
{
return () => Console.WriteLine(input);
}
}
Instead, we only allocate when we call the local function:
public static Action Test(int input)
{
if (input < int.MinValue)
{
return WriteConsoleAction(input);
}
return <>c.<>9__1_0 ?? (<>c.<>9__1_0 = <>c.<>9.<Test>b__1_0);
static Action WriteConsoleAction(int input)
{
<>c__DisplayClass1_0 <>c__DisplayClass1_ = new <>c__DisplayClass1_0();
<>c__DisplayClass1_.input = input;
return <>c__DisplayClass1_.<Test>b__2;
}
}
Creating the Action
requires capturing some state objects, and we can avoid this by moving to a static local function. I can add comments or clarify this somehow. What would help?
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.
One note, this statement return <>c.<>9__1_0 ?? (<>c.<>9__1_0 = <>c.<>9.<Test>b__1_0);
assigns the non-capturing delegate to a static field that gets reused. No additional allocations here.
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.
if there's a capture anywhere in the body of the method, the closure object is created.
TIL. This makes sense then. I'd remove the comment since it's meaningless in this context--the idea was "don't create the context if you won't need it" but if this method is called you need it.
@@ -1246,7 +1247,7 @@ private void AssignUnscheduledRequestsWithMaxWaitingRequests2(List<ScheduleRespo | |||
private void AssignUnscheduledRequestsFIFO(List<ScheduleResponse> responses, HashSet<int> idleNodes) | |||
{ | |||
// Assign requests on a first-come/first-serve basis | |||
foreach (int nodeId in idleNodes) | |||
foreach (SchedulableRequest unscheduledRequest in _schedulingData.UnscheduledRequestsWhichCanBeScheduled) |
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.
What's the motivation for the switch? Document in a comment.
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.
Enumerating over UnscheduledRequestsWhichCanBeScheduled
is more expensive than enumerating over idleNodes
(HashSet<int>
). To enumerate UnscheduledRequestsWhichCanBeScheduled
, we allocate a new enumerator each time (from the yield return) and the linked list enumeration is slightly slower too. I can add a comment to clarify if we want to keep this change.
Fixes #
Context
Changes Made
Testing
Notes