From 48814adbb3e08221e82cbf6c95a047aa0e77e4f3 Mon Sep 17 00:00:00 2001 From: "Matt Mitchell (.NET)" Date: Fri, 5 Jun 2026 14:19:37 -0700 Subject: [PATCH 1/2] Add structured desktop Helix batching Compose desktop batch commands from structured runner parts and group library Helix work items by compatibility metadata. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- eng/testing/DesktopBatchRunner.cmd | 77 ++++++++ eng/testing/DesktopBatchRunner.sh | 110 +++++++++++ src/libraries/pretest.proj | 4 +- src/libraries/sendtohelixhelp.proj | 113 +++++++++++- .../GroupWorkItemsTests.cs | 174 ++++++++++++++++++ .../HelixTestTasks.Tests.csproj | 20 ++ src/tasks/HelixTestTasks/GroupWorkItems.cs | 98 +++++++++- 7 files changed, 580 insertions(+), 16 deletions(-) create mode 100644 eng/testing/DesktopBatchRunner.cmd create mode 100644 eng/testing/DesktopBatchRunner.sh create mode 100644 src/tasks/HelixTestTasks.Tests/GroupWorkItemsTests.cs create mode 100644 src/tasks/HelixTestTasks.Tests/HelixTestTasks.Tests.csproj diff --git a/eng/testing/DesktopBatchRunner.cmd b/eng/testing/DesktopBatchRunner.cmd new file mode 100644 index 00000000000000..13b0f8831bdf7f --- /dev/null +++ b/eng/testing/DesktopBatchRunner.cmd @@ -0,0 +1,77 @@ +@echo off +setlocal enabledelayedexpansion + +set "BATCH_DIR=%CD%" +set /a SUITE_COUNT=0 +set /a FAIL_COUNT=0 +set "FOUND_ZIP=" +set "PYTHON=%HELIX_PYTHONPATH%" +if "%PYTHON%"=="" set "PYTHON=python" + +if "%HELIX_WORKITEM_UPLOAD_ROOT%"=="" ( + set "ORIGINAL_UPLOAD_ROOT=%CD%\test-results" +) else ( + set "ORIGINAL_UPLOAD_ROOT=%HELIX_WORKITEM_UPLOAD_ROOT%" +) + +echo === DesktopBatchRunner === +echo BATCH_DIR=%BATCH_DIR% +echo ORIGINAL_UPLOAD_ROOT=%ORIGINAL_UPLOAD_ROOT% + +for %%z in ("%BATCH_DIR%\*.zip") do ( + if exist "%%~fz" ( + set "FOUND_ZIP=1" + set "suiteName=%%~nz" + set "suiteDir=%BATCH_DIR%\!suiteName!" + set "suiteExitCode=0" + + echo. + echo ========================= BEGIN !suiteName! ============================= + + mkdir "!suiteDir!" >nul 2>nul + "%PYTHON%" -c "import zipfile,sys; zipfile.ZipFile(sys.argv[1]).extractall(sys.argv[2])" "%%~fz" "!suiteDir!" + if !errorlevel! neq 0 ( + echo ERROR: Failed to extract %%~fz + set "suiteExitCode=1" + ) else ( + set "HELIX_WORKITEM_UPLOAD_ROOT=!ORIGINAL_UPLOAD_ROOT!\!suiteName!" + mkdir "!HELIX_WORKITEM_UPLOAD_ROOT!" >nul 2>nul + + pushd "!suiteDir!" + call RunTests.cmd %* + set "suiteExitCode=!errorlevel!" + popd + ) + + rmdir /s /q "!suiteDir!" 2>nul + + set /a SUITE_COUNT+=1 + + if !suiteExitCode! neq 0 ( + set /a FAIL_COUNT+=1 + echo ----- FAIL !suiteName! - exit code !suiteExitCode! ----- + ) else ( + echo ----- PASS !suiteName! ----- + ) + + echo ========================= END !suiteName! =============================== + ) +) + +set "HELIX_WORKITEM_UPLOAD_ROOT=%ORIGINAL_UPLOAD_ROOT%" + +if not defined FOUND_ZIP ( + echo No .zip files found in %BATCH_DIR% + exit /b 1 +) + +echo. +echo === Batch Summary === +set /a PASS_COUNT=SUITE_COUNT-FAIL_COUNT +echo Total: %SUITE_COUNT% ^| Passed: %PASS_COUNT% ^| Failed: %FAIL_COUNT% + +if %FAIL_COUNT% neq 0 ( + exit /b 1 +) + +exit /b 0 diff --git a/eng/testing/DesktopBatchRunner.sh b/eng/testing/DesktopBatchRunner.sh new file mode 100644 index 00000000000000..6bb645c9599b7d --- /dev/null +++ b/eng/testing/DesktopBatchRunner.sh @@ -0,0 +1,110 @@ +#!/usr/bin/env bash + +if [[ -z "${HELIX_WORKITEM_UPLOAD_ROOT:-}" ]]; then + ORIGINAL_UPLOAD_ROOT="$PWD/test-results" +else + ORIGINAL_UPLOAD_ROOT="$HELIX_WORKITEM_UPLOAD_ROOT" +fi + +if [[ -n "${HELIX_PYTHONPATH:-}" ]]; then + PYTHON_CMD=("$HELIX_PYTHONPATH") +elif command -v python3 >/dev/null 2>&1; then + PYTHON_CMD=(python3) +else + PYTHON_CMD=(python) +fi + +BATCH_DIR="$PWD" +SUITE_COUNT=0 +FAIL_COUNT=0 +SUITE_NAMES=() +SUITE_EXIT_CODES=() +SUITE_DURATIONS=() + +echo "=== DesktopBatchRunner ===" +echo "BATCH_DIR=$BATCH_DIR" +echo "ORIGINAL_UPLOAD_ROOT=$ORIGINAL_UPLOAD_ROOT" + +for zipFile in "$BATCH_DIR"/*.zip; do + if [[ ! -f "$zipFile" ]]; then + echo "No .zip files found in $BATCH_DIR" + exit 1 + fi + + suiteName=$(basename "$zipFile" .zip) + suiteDir="$BATCH_DIR/$suiteName" + + echo "" + echo "========================= BEGIN $suiteName =============================" + + mkdir -p "$suiteDir" + "${PYTHON_CMD[@]}" -c "import zipfile,sys; zipfile.ZipFile(sys.argv[1]).extractall(sys.argv[2])" "$zipFile" "$suiteDir" + unzipExitCode=$? + if [[ $unzipExitCode -ne 0 ]]; then + echo "ERROR: Failed to extract $zipFile (exit code: $unzipExitCode)" + FAIL_COUNT=$((FAIL_COUNT + 1)) + SUITE_NAMES+=("$suiteName") + SUITE_EXIT_CODES+=("$unzipExitCode") + SUITE_DURATIONS+=("0") + SUITE_COUNT=$((SUITE_COUNT + 1)) + rm -rf "$suiteDir" + continue + fi + + export HELIX_WORKITEM_UPLOAD_ROOT="$ORIGINAL_UPLOAD_ROOT/$suiteName" + mkdir -p "$HELIX_WORKITEM_UPLOAD_ROOT" + + pushd "$suiteDir" >/dev/null + + chmod +x RunTests.sh + + startTime=$(date +%s) + ./RunTests.sh "$@" + suiteExitCode=$? + endTime=$(date +%s) + + popd >/dev/null + + rm -rf "$suiteDir" + + duration=$((endTime - startTime)) + + SUITE_NAMES+=("$suiteName") + SUITE_EXIT_CODES+=("$suiteExitCode") + SUITE_DURATIONS+=("$duration") + SUITE_COUNT=$((SUITE_COUNT + 1)) + + if [[ $suiteExitCode -ne 0 ]]; then + FAIL_COUNT=$((FAIL_COUNT + 1)) + echo "----- FAIL $suiteName - exit code $suiteExitCode - ${duration}s -----" + else + echo "----- PASS $suiteName - ${duration}s -----" + fi + + echo "========================= END $suiteName ===============================" +done + +export HELIX_WORKITEM_UPLOAD_ROOT="$ORIGINAL_UPLOAD_ROOT" + +echo "" +echo "=== Batch Summary ===" +printf "%-60s %-6s %s\n" "Suite" "Status" "Duration" +printf "%-60s %-6s %s\n" "-----" "------" "--------" + +for i in "${!SUITE_NAMES[@]}"; do + if [[ ${SUITE_EXIT_CODES[$i]} -eq 0 ]]; then + status="PASS" + else + status="FAIL" + fi + printf "%-60s %-6s %ss\n" "${SUITE_NAMES[$i]}" "$status" "${SUITE_DURATIONS[$i]}" +done + +echo "" +echo "Total: $SUITE_COUNT | Passed: $((SUITE_COUNT - FAIL_COUNT)) | Failed: $FAIL_COUNT" + +if [[ $FAIL_COUNT -ne 0 ]]; then + exit 1 +fi + +exit 0 diff --git a/src/libraries/pretest.proj b/src/libraries/pretest.proj index 7b53dffc05900c..7ee882806626d6 100644 --- a/src/libraries/pretest.proj +++ b/src/libraries/pretest.proj @@ -24,8 +24,8 @@ - - + + diff --git a/src/libraries/sendtohelixhelp.proj b/src/libraries/sendtohelixhelp.proj index e9d59bc6be2b28..daaf58a0c25ff9 100644 --- a/src/libraries/sendtohelixhelp.proj +++ b/src/libraries/sendtohelixhelp.proj @@ -16,6 +16,9 @@ BuildHelixCommand;StageDependenciesForHelix true $([MSBuild]::NormalizeDirectory($(ArtifactsObjDir), 'helix-staging')) + true + 20 + <_LibraryHelixBatchLargeThreshold Condition="'$(_LibraryHelixBatchLargeThreshold)' == ''">52428800 @@ -23,6 +26,13 @@ + + + - $(HelixCommand) powershell -command "dotnet dev-certs https --export-path devcerts.pfx --password PLACEHOLDER %3B $pw = ConvertTo-SecureString PLACEHOLDER -AsPlainText -Force %3B Import-PfxCertificate -FilePath devcerts.pfx -Password $pw -CertStoreLocation Cert:\LocalMachine\Root " && + $(HelixCommandPrefix) powershell -command "dotnet dev-certs https --export-path devcerts.pfx --password PLACEHOLDER %3B $pw = ConvertTo-SecureString PLACEHOLDER -AsPlainText -Force %3B Import-PfxCertificate -FilePath devcerts.pfx -Password $pw -CertStoreLocation Cert:\LocalMachine\Root " && - $(HelixCommand)call RunTests.cmd - $(HelixCommand) --runtime-path %HELIX_CORRELATION_PAYLOAD% - - $(HelixCommand)./RunTests.sh - $(HelixCommand) --runtime-path "$HELIX_CORRELATION_PAYLOAD" + call RunTests.cmd + call DesktopBatchRunner.cmd + $(HelixRunnerEntryPoint) --runtime-path %HELIX_CORRELATION_PAYLOAD% + $(HelixBatchRunnerEntryPoint) --runtime-path %HELIX_CORRELATION_PAYLOAD% + + ./RunTests.sh + chmod +x DesktopBatchRunner.sh && ./DesktopBatchRunner.sh + $(HelixRunnerEntryPoint) --runtime-path "$HELIX_CORRELATION_PAYLOAD" + $(HelixBatchRunnerEntryPoint) --runtime-path "$HELIX_CORRELATION_PAYLOAD" + + $(HelixCommandPrefix) + $(HelixCommand)$(HelixRunnerEntryPoint) + <_BatchHelixCommand Condition="'$(HelixCommandPrefix)' != ''">$(HelixCommandPrefix) + <_BatchHelixCommand>$(_BatchHelixCommand)$(HelixBatchRunnerEntryPoint) @@ -300,7 +319,7 @@ - + @@ -315,6 +334,80 @@ + + + + <_DefaultWorkItems Include="$(WorkItemArchiveWildCard)" Exclude="$(HelixCorrelationPayload)"> + true + false + Scenario=$(Scenario) + Desktop + Windows + Unix + $(RuntimeFlavor) + $(TargetOS) + $(TargetArchitecture) + normal + $(Scenario) + Normal + Stress + DesktopRuntime + + + + + + + + + <_BatchId Include="@(_BatchGroupedItem -> '%(BatchId)')" /> + <_UniqueBatchId Include="@(_BatchId->Distinct())" /> + + + + + + + + + + + + + + + + + + + $(IntermediateOutputPath)helix-batches/%(Identity).zip + $(_BatchHelixCommand) + + + diff --git a/src/tasks/HelixTestTasks.Tests/GroupWorkItemsTests.cs b/src/tasks/HelixTestTasks.Tests/GroupWorkItemsTests.cs new file mode 100644 index 00000000000000..77233540537efa --- /dev/null +++ b/src/tasks/HelixTestTasks.Tests/GroupWorkItemsTests.cs @@ -0,0 +1,174 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.DotNet.HelixTestTasks; +using Xunit; + +namespace HelixTestTasks.Tests; + +public sealed class GroupWorkItemsTests : IDisposable +{ + private readonly string _testDirectory = Path.Combine(AppContext.BaseDirectory, "GroupWorkItemsTests", Guid.NewGuid().ToString("N")); + + public void Dispose() + { + if (Directory.Exists(_testDirectory)) + { + Directory.Delete(_testDirectory, recursive: true); + } + } + + [Fact] + public void PreservesGreedyPackingWhenCompatibilityMetadataIsNotConfigured() + { + ITaskItem[] result = Execute( + batchSize: 2, + largeThreshold: 1_000, + Item("a.zip", 100), + Item("b.zip", 50), + Item("c.zip", 25)); + + Assert.Equal("0", BatchId(result, "a.zip")); + Assert.Equal("1", BatchId(result, "b.zip")); + Assert.Equal("1", BatchId(result, "c.zip")); + } + + [Fact] + public void PartitionsSmallItemsByCompatibilityMetadataBeforePacking() + { + ITaskItem normal1 = Item("normal1.zip", 100, ("Scenario", "normal")); + ITaskItem normal2 = Item("normal2.zip", 50, ("Scenario", "normal")); + ITaskItem stress1 = Item("stress1.zip", 90, ("Scenario", "stress")); + ITaskItem stress2 = Item("stress2.zip", 40, ("Scenario", "stress")); + + ITaskItem[] result = Execute( + batchSize: 2, + largeThreshold: 1_000, + compatibilityMetadataKeys: "Scenario", + normal1, + normal2, + stress1, + stress2); + + string[] normalBatchIds = { BatchId(result, "normal1.zip"), BatchId(result, "normal2.zip") }; + string[] stressBatchIds = { BatchId(result, "stress1.zip"), BatchId(result, "stress2.zip") }; + + Assert.Empty(normalBatchIds.Intersect(stressBatchIds)); + } + + [Fact] + public void BatchCompatibleFalseItemsBecomeSoloBatches() + { + ITaskItem[] result = Execute( + batchSize: 2, + largeThreshold: 1_000, + compatibilityMetadataKeys: "RunnerType", + batchCompatibleMetadataKey: "BatchCompatible", + Item("one.zip", 100, ("RunnerType", "Desktop"), ("BatchCompatible", "true")), + Item("two.zip", 90, ("RunnerType", "Desktop"), ("BatchCompatible", "true")), + Item("stress.zip", 80, ("RunnerType", "Desktop"), ("BatchCompatible", "false"))); + + Assert.NotEqual(BatchId(result, "stress.zip"), BatchId(result, "one.zip")); + Assert.NotEqual(BatchId(result, "stress.zip"), BatchId(result, "two.zip")); + Assert.Single(result.Where(item => item.GetMetadata("BatchId") == BatchId(result, "stress.zip"))); + } + + [Fact] + public void LargeItemsKeepNegativeSoloBatchIds() + { + ITaskItem[] result = Execute( + batchSize: 2, + largeThreshold: 50, + Item("large.zip", 100), + Item("small.zip", 10)); + + Assert.Equal("-1", BatchId(result, "large.zip")); + Assert.Equal("0", BatchId(result, "small.zip")); + } + + [Fact] + public void MissingCompatibilityMetadataIsSoloBatchedWithoutFailingByDefault() + { + ITaskItem[] result = Execute( + batchSize: 2, + largeThreshold: 1_000, + compatibilityMetadataKeys: "RunnerType", + Item("has-metadata.zip", 100, ("RunnerType", "Desktop")), + Item("missing-metadata.zip", 90)); + + Assert.NotEqual(BatchId(result, "has-metadata.zip"), BatchId(result, "missing-metadata.zip")); + } + + private ITaskItem[] Execute( + int batchSize, + long largeThreshold, + params ITaskItem[] items) => + Execute(batchSize, largeThreshold, compatibilityMetadataKeys: string.Empty, batchCompatibleMetadataKey: string.Empty, items); + + private static ITaskItem[] Execute( + int batchSize, + long largeThreshold, + string compatibilityMetadataKeys, + params ITaskItem[] items) => + Execute(batchSize, largeThreshold, compatibilityMetadataKeys, batchCompatibleMetadataKey: string.Empty, items); + + private static ITaskItem[] Execute( + int batchSize, + long largeThreshold, + string compatibilityMetadataKeys, + string batchCompatibleMetadataKey, + params ITaskItem[] items) + { + var task = new GroupWorkItems + { + BuildEngine = new MockBuildEngine(), + Items = items, + BatchSize = batchSize, + LargeThreshold = largeThreshold, + CompatibilityMetadataKeys = compatibilityMetadataKeys, + BatchCompatibleMetadataKey = batchCompatibleMetadataKey, + }; + + Assert.True(task.Execute()); + return task.GroupedItems; + } + + private ITaskItem Item(string name, int size, params (string key, string value)[] metadata) + { + Directory.CreateDirectory(_testDirectory); + string path = Path.Combine(_testDirectory, name); + File.WriteAllBytes(path, new byte[size]); + + var item = new TaskItem(path); + foreach ((string key, string value) in metadata) + { + item.SetMetadata(key, value); + } + + return item; + } + + private static string BatchId(IEnumerable items, string fileName) => + items.Single(item => Path.GetFileName(item.ItemSpec) == fileName).GetMetadata("BatchId"); + + private sealed class MockBuildEngine : IBuildEngine + { + public bool ContinueOnError => false; + public int LineNumberOfTaskNode => 0; + public int ColumnNumberOfTaskNode => 0; + public string ProjectFileOfTaskNode => string.Empty; + + public bool BuildProjectFile(string projectFileName, string[] targetNames, IDictionary globalProperties, IDictionary targetOutputs) => true; + public void LogCustomEvent(CustomBuildEventArgs e) { } + public void LogErrorEvent(BuildErrorEventArgs e) { } + public void LogMessageEvent(BuildMessageEventArgs e) { } + public void LogWarningEvent(BuildWarningEventArgs e) { } + } +} diff --git a/src/tasks/HelixTestTasks.Tests/HelixTestTasks.Tests.csproj b/src/tasks/HelixTestTasks.Tests/HelixTestTasks.Tests.csproj new file mode 100644 index 00000000000000..eef4a495ebf56e --- /dev/null +++ b/src/tasks/HelixTestTasks.Tests/HelixTestTasks.Tests.csproj @@ -0,0 +1,20 @@ + + + + $(NetCoreAppToolCurrent) + true + false + enable + $(NoWarn);NETSDK1023 + + + + + + + + + + + + diff --git a/src/tasks/HelixTestTasks/GroupWorkItems.cs b/src/tasks/HelixTestTasks/GroupWorkItems.cs index a66f85b69c4b74..edd84e5106261f 100644 --- a/src/tasks/HelixTestTasks/GroupWorkItems.cs +++ b/src/tasks/HelixTestTasks/GroupWorkItems.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using Microsoft.Build.Framework; using Microsoft.Build.Utilities; @@ -18,6 +19,8 @@ namespace Microsoft.DotNet.HelixTestTasks; /// public class GroupWorkItems : Task { + private static readonly char[] s_compatibilityMetadataKeySeparators = { ';' }; + [Required] public ITaskItem[] Items { get; set; } = Array.Empty(); @@ -25,6 +28,12 @@ public class GroupWorkItems : Task public long LargeThreshold { get; set; } = 52428800L; // 50 MB + public string CompatibilityMetadataKeys { get; set; } = string.Empty; + + public string BatchCompatibleMetadataKey { get; set; } = string.Empty; + + public bool StrictCompatibilityMetadata { get; set; } + [Output] public ITaskItem[] GroupedItems { get; set; } = Array.Empty(); @@ -50,9 +59,15 @@ public override bool Execute() var result = new List(); int negativeBatchId = -1; + int nextBatchId = 0; + string[] compatibilityKeys = CompatibilityMetadataKeys + .Split(s_compatibilityMetadataKeySeparators, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + bool hasCompatibilityKeys = compatibilityKeys.Length > 0; + string batchCompatibleMetadataKey = BatchCompatibleMetadataKey.Trim(); // Separate large items (each gets its own batch) - var smallItems = new List<(ITaskItem item, long size)>(); + var partitionedSmallItems = new Dictionary>(StringComparer.Ordinal); + var soloSmallItems = new List<(ITaskItem item, long size)>(); foreach (var entry in itemsWithSize) { if (entry.size > LargeThreshold) @@ -64,12 +79,25 @@ public override bool Execute() } else { - smallItems.Add(entry); + if (IsSolo(entry.item, batchCompatibleMetadataKey, compatibilityKeys, out string partitionKey)) + { + soloSmallItems.Add(entry); + } + else + { + if (!partitionedSmallItems.TryGetValue(partitionKey, out List<(ITaskItem item, long size)>? partitionItems)) + { + partitionItems = new List<(ITaskItem item, long size)>(); + partitionedSmallItems.Add(partitionKey, partitionItems); + } + + partitionItems.Add(entry); + } } } // Greedy bin-packing for small items - if (smallItems.Count > 0) + foreach (List<(ITaskItem item, long size)> smallItems in partitionedSmallItems.Values) { int numBatches = Math.Min(BatchSize, smallItems.Count); var batchSizes = new long[numBatches]; @@ -88,15 +116,77 @@ public override bool Execute() } batchSizes[minIdx] += entry.size; var newItem = new TaskItem(entry.item); - newItem.SetMetadata("BatchId", minIdx.ToString()); + newItem.SetMetadata("BatchId", (nextBatchId + minIdx).ToString()); batchAssignments[minIdx].Add(newItem); } for (int i = 0; i < numBatches; i++) result.AddRange(batchAssignments[i]); + + nextBatchId += numBatches; + } + + foreach (var entry in soloSmallItems) + { + var newItem = new TaskItem(entry.item); + newItem.SetMetadata("BatchId", nextBatchId.ToString()); + nextBatchId++; + result.Add(newItem); } GroupedItems = result.ToArray(); return !Log.HasLoggedErrors; } + + private bool IsSolo(ITaskItem item, string batchCompatibleMetadataKey, string[] compatibilityKeys, out string partitionKey) + { + partitionKey = string.Empty; + + if (!string.IsNullOrEmpty(batchCompatibleMetadataKey)) + { + string batchCompatible = item.GetMetadata(batchCompatibleMetadataKey); + if (!string.IsNullOrEmpty(batchCompatible) && + !string.Equals(batchCompatible, "true", StringComparison.OrdinalIgnoreCase)) + { + Log.LogMessage( + MessageImportance.Low, + "Keeping '{0}' in a solo batch because metadata '{1}' is '{2}'.", + item.ItemSpec, + batchCompatibleMetadataKey, + batchCompatible); + return true; + } + } + + if (compatibilityKeys.Length == 0) + { + return false; + } + + string[] values = new string[compatibilityKeys.Length]; + for (int i = 0; i < compatibilityKeys.Length; i++) + { + string key = compatibilityKeys[i]; + string value = item.GetMetadata(key); + if (string.IsNullOrEmpty(value)) + { + string message = $"Keeping '{item.ItemSpec}' in a solo batch because required compatibility metadata '{key}' is missing."; + if (StrictCompatibilityMetadata) + { + Log.LogError(message); + } + else + { + Log.LogMessage(MessageImportance.High, message); + } + + return true; + } + + values[i] = value; + } + + partitionKey = string.Join('\u001f', compatibilityKeys.Zip(values, static (key, value) => key + "=" + value)); + return false; + } } From cb0140a0386ebee083b38674dabbcdbba1b8571d Mon Sep 17 00:00:00 2001 From: "Matt Mitchell (.NET)" Date: Fri, 5 Jun 2026 14:22:58 -0700 Subject: [PATCH 2/2] Preserve stress Helix timeouts when batching Emit non-batch-compatible desktop library work items directly so stress scenarios keep the original runner and timeout instead of flowing through ComputeBatchTimeout. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/libraries/sendtohelixhelp.proj | 36 +++++++++++++++++++----------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/src/libraries/sendtohelixhelp.proj b/src/libraries/sendtohelixhelp.proj index daaf58a0c25ff9..d1c5bd342411b4 100644 --- a/src/libraries/sendtohelixhelp.proj +++ b/src/libraries/sendtohelixhelp.proj @@ -355,10 +355,20 @@ Stress DesktopRuntime + + <_BatchCandidateWorkItems Include="@(_DefaultWorkItems)" Condition="'%(_DefaultWorkItems.BatchCompatible)' == 'true'" /> + <_NonBatchWorkItems Include="@(_DefaultWorkItems)" Condition="'%(_DefaultWorkItems.BatchCompatible)' != 'true'" /> + + + %(_NonBatchWorkItems.Identity) + $(HelixCommand) + $(_workItemTimeout) + $(SuperPmiCollectionName).$(SuperPmiCollectionType).$(TargetOS).$(TargetArchitecture).$(Configuration).mch;$(SuperPmiCollectionName).$(SuperPmiCollectionType).$(TargetOS).$(TargetArchitecture).$(Configuration).log + - - + <_BatchId Include="@(_BatchGroupedItem -> '%(BatchId)')" /> <_UniqueBatchId Include="@(_BatchId->Distinct())" /> - - - - + - - - - - + $(IntermediateOutputPath)helix-batches/%(Identity).zip $(_BatchHelixCommand)