diff --git a/test/HelixTasks/HelixSchedulingConfig.cs b/test/HelixTasks/HelixSchedulingConfig.cs
new file mode 100644
index 000000000000..353e84b6706b
--- /dev/null
+++ b/test/HelixTasks/HelixSchedulingConfig.cs
@@ -0,0 +1,105 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Reflection;
+using System.Xml.Linq;
+
+namespace Microsoft.DotNet.SdkCustomHelix.Sdk;
+
+///
+/// Loads per-assembly scheduling hints from the embedded HelixSchedulingConfig.xml resource.
+///
+internal sealed class HelixSchedulingConfig
+{
+ private const double DefaultEstimatedSecondsPerMethod = 2.0;
+ private const double DefaultMethodLimitMultiplier = 1.0;
+
+ private readonly Dictionary _configs;
+
+ private HelixSchedulingConfig(Dictionary configs)
+ {
+ _configs = configs;
+ }
+
+ internal readonly struct AssemblyConfig
+ {
+ internal readonly double MethodLimitMultiplier;
+ internal readonly double EstimatedSecondsPerMethod;
+
+ internal AssemblyConfig(double methodLimitMultiplier, double estimatedSecondsPerMethod)
+ {
+ MethodLimitMultiplier = methodLimitMultiplier;
+ EstimatedSecondsPerMethod = estimatedSecondsPerMethod;
+ }
+ }
+
+ ///
+ /// Loads the config from the embedded XML resource.
+ ///
+ internal static HelixSchedulingConfig Load()
+ {
+ var configs = new Dictionary(StringComparer.OrdinalIgnoreCase);
+
+ using var stream = Assembly.GetExecutingAssembly()
+ .GetManifestResourceStream("HelixSchedulingConfig.xml");
+
+ if (stream is null)
+ {
+ return new HelixSchedulingConfig(configs);
+ }
+
+ var doc = XDocument.Load(stream);
+ foreach (var element in doc.Root!.Elements("Assembly"))
+ {
+ var name = element.Attribute("Name")?.Value;
+ if (string.IsNullOrEmpty(name))
+ {
+ continue;
+ }
+
+ var multiplier = DefaultMethodLimitMultiplier;
+ if (double.TryParse(element.Attribute("MethodLimitMultiplier")?.Value, out var m))
+ {
+ multiplier = m;
+ }
+
+ var secondsPerMethod = DefaultEstimatedSecondsPerMethod;
+ if (double.TryParse(element.Attribute("EstimatedSecondsPerMethod")?.Value, out var s))
+ {
+ secondsPerMethod = s;
+ }
+
+ configs[name] = new AssemblyConfig(multiplier, secondsPerMethod);
+ }
+
+ return new HelixSchedulingConfig(configs);
+ }
+
+ ///
+ /// Gets the method limit multiplier for an assembly (defaults to 1.0).
+ ///
+ internal double GetMethodLimitMultiplier(string assemblyName)
+ {
+ return _configs.TryGetValue(assemblyName, out var config)
+ ? config.MethodLimitMultiplier
+ : DefaultMethodLimitMultiplier;
+ }
+
+ ///
+ /// Gets the estimated seconds per method for an assembly (defaults to 2.0).
+ ///
+ internal double GetEstimatedSecondsPerMethod(string assemblyName)
+ {
+ return _configs.TryGetValue(assemblyName, out var config)
+ ? config.EstimatedSecondsPerMethod
+ : DefaultEstimatedSecondsPerMethod;
+ }
+
+ ///
+ /// Estimates the duration of a work item based on its method count and assembly.
+ ///
+ internal double EstimateDuration(string assemblyName, int methodCount)
+ {
+ return methodCount * GetEstimatedSecondsPerMethod(assemblyName);
+ }
+}
diff --git a/test/HelixTasks/HelixSchedulingConfig.xml b/test/HelixTasks/HelixSchedulingConfig.xml
new file mode 100644
index 000000000000..889820e56b3a
--- /dev/null
+++ b/test/HelixTasks/HelixSchedulingConfig.xml
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/HelixTasks/HelixTasks.csproj b/test/HelixTasks/HelixTasks.csproj
index 43c679c421ef..1110bb010d37 100644
--- a/test/HelixTasks/HelixTasks.csproj
+++ b/test/HelixTasks/HelixTasks.csproj
@@ -7,6 +7,10 @@
Microsoft.DotNet.SDK.Build.Helix
+
+
+
+
diff --git a/test/HelixTasks/SDKCustomCreateXUnitWorkItemsWithTestExclusion.cs b/test/HelixTasks/SDKCustomCreateXUnitWorkItemsWithTestExclusion.cs
index c8bfa9ef0dc4..83a44ce97042 100644
--- a/test/HelixTasks/SDKCustomCreateXUnitWorkItemsWithTestExclusion.cs
+++ b/test/HelixTasks/SDKCustomCreateXUnitWorkItemsWithTestExclusion.cs
@@ -13,6 +13,8 @@ namespace Microsoft.DotNet.SdkCustomHelix.Sdk
///
public class SDKCustomCreateXUnitWorkItemsWithTestExclusion : Build.Utilities.Task
{
+ private HelixSchedulingConfig _schedulingConfig = null!;
+
///
/// An array of XUnit project workitems containing the following metadata:
/// - [Required] PublishDirectory: the publish output directory of the XUnit project
@@ -81,10 +83,24 @@ private async Task ExecuteAsync()
return;
}
- XUnitWorkItems = (await Task.WhenAll(XUnitProjects.Select(PrepareWorkItem)))
+ _schedulingConfig = HelixSchedulingConfig.Load();
+
+ var workItems = (await Task.WhenAll(XUnitProjects.Select(PrepareWorkItem)))
.SelectMany(i => i ?? new())
.Where(wi => wi != null)
- .ToArray();
+ .ToList();
+
+ // Sort by estimated duration descending (longest-first / LPT scheduling).
+ // This ensures Helix machines pick up the heaviest items first, reducing
+ // the chance of long items stacking on the same machine late in the run.
+ workItems.Sort((a, b) =>
+ {
+ var durA = double.TryParse(a.GetMetadata("EstimatedDurationSeconds"), out var da) ? da : 0;
+ var durB = double.TryParse(b.GetMetadata("EstimatedDurationSeconds"), out var db) ? db : 0;
+ return durB.CompareTo(durA);
+ });
+
+ XUnitWorkItems = workItems.ToArray();
return;
}
@@ -152,7 +168,11 @@ private async Task ExecuteAsync()
msbuildAdditionalSdkResolverFolder = "";
}
- var scheduler = new AssemblyScheduler(methodLimit: !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("TestFullMSBuild")) ? 32 : 16);
+ int baseMethodLimit = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("TestFullMSBuild")) ? 32 : 16;
+ double multiplier = _schedulingConfig.GetMethodLimitMultiplier(assemblyName);
+ int methodLimit = Math.Max(1, (int)(baseMethodLimit * multiplier));
+
+ var scheduler = new AssemblyScheduler(methodLimit: methodLimit);
var assemblyPartitionInfos = scheduler.Schedule(targetPath);
var partitionedWorkItem = new List();
@@ -176,12 +196,14 @@ private async Task ExecuteAsync()
Log.LogMessage($"Creating work item with properties Identity: {assemblyName}, PayloadDirectory: {publishDirectory}, Command: {command}");
+ double estimatedDuration = _schedulingConfig.EstimateDuration(assemblyName, methodLimit);
partitionedWorkItem.Add(new Microsoft.Build.Utilities.TaskItem(assemblyPartitionInfo.DisplayName + testIdentityDifferentiator, new Dictionary()
{
{ "Identity", assemblyPartitionInfo.DisplayName + testIdentityDifferentiator},
{ "PayloadDirectory", publishDirectory },
{ "Command", command },
{ "Timeout", timeout.ToString() },
+ { "EstimatedDurationSeconds", estimatedDuration.ToString("F1") },
}));
}
diff --git a/test/dotnet.Tests/CommandTests/Run/RunFileTests_CscOnlyAndApi.cs b/test/dotnet.Tests/CommandTests/Run/RunFileTests_CscOnlyAndApi.cs
index ce7558ef2ef0..264614ef46ff 100644
--- a/test/dotnet.Tests/CommandTests/Run/RunFileTests_CscOnlyAndApi.cs
+++ b/test/dotnet.Tests/CommandTests/Run/RunFileTests_CscOnlyAndApi.cs
@@ -361,7 +361,7 @@ public void UpToDate_DefaultItems_Razor()
.And.HaveStdOutContaining("error CS0246");
}
- [Fact]
+ [Fact(Skip = "https://github.com/dotnet/sdk/issues/53930 - Temporarily disabled for Helix scheduling analysis")]
public void CscOnly()
{
var testInstance = TestAssetsManager.CreateTestDirectory(baseDirectory: OutOfTreeBaseDirectory);