Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 105 additions & 0 deletions test/HelixTasks/HelixSchedulingConfig.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Loads per-assembly scheduling hints from the embedded HelixSchedulingConfig.xml resource.
/// </summary>
internal sealed class HelixSchedulingConfig
{
private const double DefaultEstimatedSecondsPerMethod = 2.0;
private const double DefaultMethodLimitMultiplier = 1.0;

private readonly Dictionary<string, AssemblyConfig> _configs;

private HelixSchedulingConfig(Dictionary<string, AssemblyConfig> 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;
}
}

/// <summary>
/// Loads the config from the embedded XML resource.
/// </summary>
internal static HelixSchedulingConfig Load()
{
var configs = new Dictionary<string, AssemblyConfig>(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);
}

/// <summary>
/// Gets the method limit multiplier for an assembly (defaults to 1.0).
/// </summary>
internal double GetMethodLimitMultiplier(string assemblyName)
{
return _configs.TryGetValue(assemblyName, out var config)
? config.MethodLimitMultiplier
: DefaultMethodLimitMultiplier;
}

/// <summary>
/// Gets the estimated seconds per method for an assembly (defaults to 2.0).
/// </summary>
internal double GetEstimatedSecondsPerMethod(string assemblyName)
{
return _configs.TryGetValue(assemblyName, out var config)
? config.EstimatedSecondsPerMethod
: DefaultEstimatedSecondsPerMethod;
}

/// <summary>
/// Estimates the duration of a work item based on its method count and assembly.
/// </summary>
internal double EstimateDuration(string assemblyName, int methodCount)
{
return methodCount * GetEstimatedSecondsPerMethod(assemblyName);
}
}
43 changes: 43 additions & 0 deletions test/HelixTasks/HelixSchedulingConfig.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<!--
Helix Work Item Scheduling Configuration
=========================================
Generated from analysis of build 1382230 (TestBuild: linux x64, ubuntu.2204.amd64.open).

MethodLimitMultiplier: Adjusts the assembly partitioning chunk size relative to the base
method limit (16 normal, 32 for TestFullMSBuild). Higher values produce fewer, larger
work items; lower values produce more, finer-grained work items.

EstimatedSecondsPerMethod: Used to estimate each work item's duration for longest-first
(LPT) scheduling. Items with the highest estimated cost are submitted first so Helix
machines pick them up before short items. Assemblies not listed use a default of 2.0.

To regenerate: run a CI build, query Helix for per-work-item durations, and compute
avg_duration / base_method_limit for each assembly.
-->
<HelixSchedulingConfig>
<!-- Heavy assemblies: long per-method cost, need finer splitting or prioritization -->
<Assembly Name="Microsoft.NET.Publish.Tests.dll" EstimatedSecondsPerMethod="22.0" />
<Assembly Name="dotnet-new.IntegrationTests.dll" EstimatedSecondsPerMethod="14.0" />
<Assembly Name="EndToEnd.Tests.dll" EstimatedSecondsPerMethod="12.0" />
<Assembly Name="Microsoft.NET.Sdk.BlazorWebAssembly.Tests.dll" EstimatedSecondsPerMethod="10.0" />
<Assembly Name="dotnet-watch.Tests.dll" EstimatedSecondsPerMethod="9.5" />
<Assembly Name="Microsoft.NET.Sdk.Razor.Tests.dll" EstimatedSecondsPerMethod="8.0" />
<Assembly Name="Microsoft.NET.Restore.Tests.dll" EstimatedSecondsPerMethod="8.0" />
<Assembly Name="Microsoft.NET.Sdk.Web.Tests.dll" EstimatedSecondsPerMethod="7.5" />
<Assembly Name="dotnet.Tests.dll" MethodLimitMultiplier="2" EstimatedSecondsPerMethod="7.0" />
<Assembly Name="Microsoft.NET.Build.Tests.dll" MethodLimitMultiplier="2" EstimatedSecondsPerMethod="7.0" />
<Assembly Name="Microsoft.WebTools.AspireService.Tests.dll" EstimatedSecondsPerMethod="7.0" />
<Assembly Name="Microsoft.NET.ToolPack.Tests.dll" EstimatedSecondsPerMethod="6.5" />
<Assembly Name="Microsoft.NET.Pack.Tests.dll" EstimatedSecondsPerMethod="5.5" />
<Assembly Name="Microsoft.NET.Build.Containers.UnitTests.dll" EstimatedSecondsPerMethod="5.0" />
<Assembly Name="ArgumentForwarding.Tests.dll" EstimatedSecondsPerMethod="4.5" />

<!-- Oversharded assemblies: many tiny items, consolidate into fewer chunks -->
<Assembly Name="Microsoft.CodeAnalysis.NetAnalyzers.UnitTests.dll" MethodLimitMultiplier="5" EstimatedSecondsPerMethod="2.5" />
<Assembly Name="Microsoft.NET.Sdk.StaticWebAssets.Tests.dll" MethodLimitMultiplier="4" EstimatedSecondsPerMethod="3.5" />
<Assembly Name="Microsoft.TemplateEngine.Cli.UnitTests.dll" MethodLimitMultiplier="11" EstimatedSecondsPerMethod="1.0" />
<Assembly Name="Microsoft.NET.Build.Tasks.Tests.dll" MethodLimitMultiplier="11" EstimatedSecondsPerMethod="1.0" />
<Assembly Name="Microsoft.DotNet.ApiDiff.Tests.dll" MethodLimitMultiplier="8" EstimatedSecondsPerMethod="1.0" />
<Assembly Name="Microsoft.DotNet.ApiCompatibility.Tests.dll" MethodLimitMultiplier="9" EstimatedSecondsPerMethod="1.0" />
<Assembly Name="Microsoft.NET.Sdk.Razor.Tool.Tests.dll" MethodLimitMultiplier="4" EstimatedSecondsPerMethod="1.0" />
</HelixSchedulingConfig>
4 changes: 4 additions & 0 deletions test/HelixTasks/HelixTasks.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
<RootNamespace>Microsoft.DotNet.SDK.Build.Helix</RootNamespace>
</PropertyGroup>

<ItemGroup>
<EmbeddedResource Include="HelixSchedulingConfig.xml" LogicalName="HelixSchedulingConfig.xml" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Build" ExcludeAssets="runtime;buildTransitive" />
<PackageReference Include="Microsoft.Build.Utilities.Core" ExcludeAssets="runtime;buildTransitive" />
Expand Down
28 changes: 25 additions & 3 deletions test/HelixTasks/SDKCustomCreateXUnitWorkItemsWithTestExclusion.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ namespace Microsoft.DotNet.SdkCustomHelix.Sdk
/// </summary>
public class SDKCustomCreateXUnitWorkItemsWithTestExclusion : Build.Utilities.Task
{
private HelixSchedulingConfig _schedulingConfig = null!;

/// <summary>
/// An array of XUnit project workitems containing the following metadata:
/// - [Required] PublishDirectory: the publish output directory of the XUnit project
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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<ITaskItem>();
Expand All @@ -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<string, string>()
{
{ "Identity", assemblyPartitionInfo.DisplayName + testIdentityDifferentiator},
{ "PayloadDirectory", publishDirectory },
{ "Command", command },
{ "Timeout", timeout.ToString() },
{ "EstimatedDurationSeconds", estimatedDuration.ToString("F1") },
}));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading