From d8fb6f4b09efbf874d3a898d19dc9f33e482e3e1 Mon Sep 17 00:00:00 2001 From: Alex Williams-Ferreira Date: Mon, 20 Jul 2026 13:59:46 -0700 Subject: [PATCH 1/2] Fix LZBench timeout and result ownership Run the benchmark as the VC user and keep its result file in the VC-owned temp directory so ARM64 cleanup does not cross privilege boundaries. Report cancellation as a workload failure instead of a successful execution. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: f3bc03fa-5da5-4a69-94e1-d6adc0cbe5db --- .../CompressionProfileTests.cs | 2 +- .../Lzbench/LzbenchExecutorTests.cs | 73 +++++++++++++- .../Lzbench/LzbenchExecutor.cs | 99 ++++++++++++------- .../Lzbench/lzbenchexecutor.sh | 5 +- .../profiles/PERF-COMPRESSION-LZBENCH.json | 2 +- 5 files changed, 138 insertions(+), 43 deletions(-) diff --git a/src/VirtualClient/VirtualClient.Actions.FunctionalTests/CompressionProfileTests.cs b/src/VirtualClient/VirtualClient.Actions.FunctionalTests/CompressionProfileTests.cs index 0640cc2fca..0f8e02c489 100644 --- a/src/VirtualClient/VirtualClient.Actions.FunctionalTests/CompressionProfileTests.cs +++ b/src/VirtualClient/VirtualClient.Actions.FunctionalTests/CompressionProfileTests.cs @@ -185,7 +185,7 @@ private IEnumerable GetLZBenchProfileExpectedCommands(PlatformID platfor $"sudo make", $"wget https://sun.aei.polsl.pl//~sdeor/corpus/silesia.zip", $"sudo unzip silesia.zip -d silesia", - $"sudo bash \"/home/user/tools/VirtualClient/scripts/lzbench/lzbenchexecutor.sh\" \"-t16,16 -eall -o4 -r /home/user/tools/VirtualClient/packages/lzbench/silesia\"" + $"bash \"/home/user/tools/VirtualClient/scripts/lzbench/lzbenchexecutor.sh\" \"-t16,16 -eall -o4 -r /home/user/tools/VirtualClient/packages/lzbench/silesia\" \"/home/user/tools/VirtualClient/temp/lzbench/results-summary.csv\"" }; } } diff --git a/src/VirtualClient/VirtualClient.Actions.UnitTests/Lzbench/LzbenchExecutorTests.cs b/src/VirtualClient/VirtualClient.Actions.UnitTests/Lzbench/LzbenchExecutorTests.cs index af6f9d50b7..8fdf2e96f7 100644 --- a/src/VirtualClient/VirtualClient.Actions.UnitTests/Lzbench/LzbenchExecutorTests.cs +++ b/src/VirtualClient/VirtualClient.Actions.UnitTests/Lzbench/LzbenchExecutorTests.cs @@ -38,6 +38,8 @@ public void SetupDefaultBehavior() this.mockFixture.File.Reset(); this.mockFixture.File.Setup(f => f.Exists(It.IsAny())) .Returns(true); + this.mockFixture.File.Setup(f => f.ReadAllTextAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(MockFixture.ReadFile(MockFixture.ExamplesDirectory, "Lzbench", "LzbenchResultsExample.csv")); this.mockFixture.Directory.Setup(f => f.Exists(It.IsAny())) .Returns(true); @@ -164,7 +166,7 @@ public async Task LzbenchExecutorRunsTheExpectedWorkloadCommand() }; string mockPackagePath = this.mockPackage.Path; - string expectedCommand = $"sudo bash \"{this.mockFixture.PlatformSpecifics.GetScriptPath("lzbench", "lzbenchexecutor.sh")}\" \"testOption1 testOption2 {this.mockFixture.PlatformSpecifics.Combine(mockPackagePath, "silesia")}\""; + string expectedCommand = $"bash \"{this.mockFixture.PlatformSpecifics.GetScriptPath("lzbench", "lzbenchexecutor.sh")}\" \"testOption1 testOption2 {this.mockFixture.PlatformSpecifics.Combine(mockPackagePath, "silesia")}\" \"{this.mockFixture.PlatformSpecifics.GetTempPath("lzbench", "results-summary.csv")}\""; bool commandExecuted = false; this.mockFixture.ProcessManager.OnCreateProcess = (exe, arguments, workingDir) => @@ -215,7 +217,7 @@ public async Task LzbenchExecutorExecutesTheCorrectCommandsWithInstallationIfInp $"sudo make", $"sudo wget https://sun.aei.polsl.pl//~sdeor/corpus/silesia.zip", $"sudo unzip silesia.zip -d silesia", - $"sudo bash \"{this.mockFixture.PlatformSpecifics.GetScriptPath("lzbench", "lzbenchexecutor.sh")}\" \"testOption1 testOption2 {this.mockFixture.PlatformSpecifics.Combine(mockPackagePath, "silesia")}\"" + $"bash \"{this.mockFixture.PlatformSpecifics.GetScriptPath("lzbench", "lzbenchexecutor.sh")}\" \"testOption1 testOption2 {this.mockFixture.PlatformSpecifics.Combine(mockPackagePath, "silesia")}\" \"{this.mockFixture.PlatformSpecifics.GetTempPath("lzbench", "results-summary.csv")}\"" }; int processCount = 0; @@ -260,7 +262,7 @@ public async Task LzbenchExecutorExecutesTheCorrectCommandsWithInstallationIfInp $"sudo rm -rf \"{mockPackagePath}\"", $"sudo git clone -b v1.8.1 https://github.com/inikep/lzbench.git", $"sudo make", - $"sudo bash \"{this.mockFixture.PlatformSpecifics.GetScriptPath("lzbench", "lzbenchexecutor.sh")}\" \"testOption1 testOption2 Test1.zip Test2.txt\"", + $"bash \"{this.mockFixture.PlatformSpecifics.GetScriptPath("lzbench", "lzbenchexecutor.sh")}\" \"testOption1 testOption2 Test1.zip Test2.txt\" \"{this.mockFixture.PlatformSpecifics.GetTempPath("lzbench", "results-summary.csv")}\"", }; int processCount = 0; @@ -302,7 +304,7 @@ public async Task LzbenchExecutorSkipsInitializationOfTheWorkloadForExecutionAft string mockPackagePath = this.mockPackage.Path; List expectedCommands = new List { - $"sudo bash \"{this.mockFixture.PlatformSpecifics.GetScriptPath("lzbench", "lzbenchexecutor.sh")}\" \"testOption1 testOption2 Test1.zip Test2.txt\"" + $"bash \"{this.mockFixture.PlatformSpecifics.GetScriptPath("lzbench", "lzbenchexecutor.sh")}\" \"testOption1 testOption2 Test1.zip Test2.txt\" \"{this.mockFixture.PlatformSpecifics.GetTempPath("lzbench", "results-summary.csv")}\"" }; int processCount = 0; @@ -337,6 +339,69 @@ public async Task LzbenchExecutorSkipsInitializationOfTheWorkloadForExecutionAft Assert.IsTrue(processCount == 1); } + [Test] + public async Task LzbenchExecutorCreatesAndCleansUpResultsUsingTheCurrentUser() + { + string resultsDirectory = this.mockFixture.PlatformSpecifics.GetTempPath("lzbench"); + string resultsFile = this.mockFixture.PlatformSpecifics.Combine(resultsDirectory, "results-summary.csv"); + this.mockFixture.Directory.Setup(directory => directory.Exists(resultsDirectory)).Returns(false); + this.mockFixture.StateManager.OnGetState().ReturnsAsync(JObject.FromObject(new LzbenchExecutor.LzbenchState() + { + LzbenchInitialized = true + })); + + using (TestLzbenchExecutor executor = new TestLzbenchExecutor(this.mockFixture.Dependencies, this.mockFixture.Parameters)) + { + await executor.ExecuteAsync(CancellationToken.None).ConfigureAwait(false); + } + + this.mockFixture.Directory.Verify(directory => directory.CreateDirectory(resultsDirectory), Times.Once); + this.mockFixture.File.Verify(file => file.Delete(resultsFile), Times.Once); + } + + [Test] + public async Task LzbenchExecutorReportsCancellationAsAWorkloadFailure() + { + using (CancellationTokenSource cancellationSource = new CancellationTokenSource()) + { + this.mockFixture.StateManager.OnGetState().ReturnsAsync(JObject.FromObject(new LzbenchExecutor.LzbenchState() + { + LzbenchInitialized = true + })); + this.mockFixture.ProcessManager.OnCreateProcess = (exe, arguments, workingDir) => + { + return new InMemoryProcess + { + StartInfo = new ProcessStartInfo + { + FileName = exe, + Arguments = arguments + }, + ExitCode = 0, + OnStart = () => + { + cancellationSource.Cancel(); + return true; + }, + OnHasExited = () => true + }; + }; + + using (TestLzbenchExecutor executor = new TestLzbenchExecutor(this.mockFixture.Dependencies, this.mockFixture.Parameters)) + { + await executor.ExecuteAsync(cancellationSource.Token).ConfigureAwait(false); + } + + IEnumerable statusMetrics = this.mockFixture.Logger + .Where(entry => entry.Item3 is EventContext context + && context.Properties.ContainsKey("metricName")) + .Select(entry => ((EventContext)entry.Item3).Properties["metricName"].ToString()); + + CollectionAssert.Contains(statusMetrics, "Failed"); + CollectionAssert.DoesNotContain(statusMetrics, "Succeeded"); + } + } + private class TestLzbenchExecutor : LzbenchExecutor { public TestLzbenchExecutor(IServiceCollection dependencies, IDictionary parameters) diff --git a/src/VirtualClient/VirtualClient.Actions/Lzbench/LzbenchExecutor.cs b/src/VirtualClient/VirtualClient.Actions/Lzbench/LzbenchExecutor.cs index b8a62f03af..e75d5f59ae 100644 --- a/src/VirtualClient/VirtualClient.Actions/Lzbench/LzbenchExecutor.cs +++ b/src/VirtualClient/VirtualClient.Actions/Lzbench/LzbenchExecutor.cs @@ -5,9 +5,7 @@ namespace VirtualClient.Actions { using System; using System.Collections.Generic; - using System.IO; using System.IO.Abstractions; - using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; @@ -101,6 +99,22 @@ private string LzbenchScriptPath } } + private string ResultsDirectory + { + get + { + return this.PlatformSpecifics.GetTempPath("lzbench"); + } + } + + private string ResultsFilePath + { + get + { + return this.Combine(this.ResultsDirectory, "results-summary.csv"); + } + } + /// /// Executes the Lzbench workload. /// @@ -108,16 +122,29 @@ protected override async Task ExecuteAsync(EventContext telemetryContext, Cancel { using (BackgroundOperations profiling = BackgroundOperations.BeginProfiling(this, cancellationToken)) { + if (!this.fileSystem.Directory.Exists(this.ResultsDirectory)) + { + this.fileSystem.Directory.CreateDirectory(this.ResultsDirectory); + } + string commandLineArguments = this.GetCommandLineArguments(); // Note: // We are attempting to add in a common method for execution of commands as we have seen throughout many executors. // In the near term, we are going to add this in slowly and incrementally in order to avoid raising the likelihood // of regressions. - using (IProcessProxy process = await this.ExecuteCommandAsync("bash", $"\"{this.LzbenchScriptPath}\" \"{commandLineArguments}\"", this.LzbenchDirectory, telemetryContext, cancellationToken, runElevated: true)) + try { - if (!cancellationToken.IsCancellationRequested) + using (IProcessProxy process = await this.ExecuteCommandAsync( + "bash", + $"\"{this.LzbenchScriptPath}\" \"{commandLineArguments}\" \"{this.ResultsFilePath}\"", + this.LzbenchDirectory, + telemetryContext, + cancellationToken, + runElevated: false)) { + cancellationToken.ThrowIfCancellationRequested(); + if (process.IsErrored()) { await this.LogProcessDetailsAsync(process, telemetryContext, "LZbench"); @@ -125,8 +152,18 @@ protected override async Task ExecuteAsync(EventContext telemetryContext, Cancel } await this.CaptureMetricsAsync(process, commandLineArguments, telemetryContext, cancellationToken); + cancellationToken.ThrowIfCancellationRequested(); } } + catch (OperationCanceledException exc) when (cancellationToken.IsCancellationRequested) + { + // A normal cancellation is success-shaped in component status telemetry. Lzbench must + // instead report failure when it is cancelled before producing complete results. + throw new WorkloadException( + "The Lzbench workload was cancelled before it completed and produced results.", + exc, + ErrorReason.WorkloadFailed); + } } } @@ -166,39 +203,33 @@ protected override async Task InitializeAsync(EventContext telemetryContext, Can private async Task CaptureMetricsAsync(IProcessProxy process, string commandArguments, EventContext telemetryContext, CancellationToken cancellationToken) { - if (!cancellationToken.IsCancellationRequested) - { - this.MetadataContract.AddForScenario( - "Lzbench", - process.FullCommand(), - toolVersion: null); + cancellationToken.ThrowIfCancellationRequested(); - this.MetadataContract.Apply(telemetryContext); + this.MetadataContract.AddForScenario( + "Lzbench", + process.FullCommand(), + toolVersion: null); - string[] resultsFiles = this.fileSystem.Directory.GetFiles(this.LzbenchDirectory, "results-summary.csv", SearchOption.AllDirectories); + this.MetadataContract.Apply(telemetryContext); - foreach (string file in resultsFiles) - { - KeyValuePair results = await this.LoadResultsAsync(file, cancellationToken); - await this.LogProcessDetailsAsync(process, telemetryContext, "LZbench", logToFile: true, results: results); - - LzbenchMetricsParser parser = new LzbenchMetricsParser(results.Value); - IList metrics = parser.Parse(); - - this.Logger.LogMetrics( - "Lzbench", - "Lzbench", - process.StartTime, - process.ExitTime, - metrics, - null, - commandArguments, - this.Tags, - telemetryContext); - - await this.fileSystem.File.DeleteAsync(file); - } - } + KeyValuePair results = await this.LoadResultsAsync(this.ResultsFilePath, cancellationToken); + await this.LogProcessDetailsAsync(process, telemetryContext, "LZbench", logToFile: true, results: results); + + LzbenchMetricsParser parser = new LzbenchMetricsParser(results.Value); + IList metrics = parser.Parse(); + + this.Logger.LogMetrics( + "Lzbench", + "Lzbench", + process.StartTime, + process.ExitTime, + metrics, + null, + commandArguments, + this.Tags, + telemetryContext); + + await this.fileSystem.File.DeleteAsync(this.ResultsFilePath); } private async Task ExecuteCommandAsync(string pathToExe, string commandLineArguments, string workingDirectory, CancellationToken cancellationToken) diff --git a/src/VirtualClient/VirtualClient.Actions/Lzbench/lzbenchexecutor.sh b/src/VirtualClient/VirtualClient.Actions/Lzbench/lzbenchexecutor.sh index a2dcd274a6..256337330c 100644 --- a/src/VirtualClient/VirtualClient.Actions/Lzbench/lzbenchexecutor.sh +++ b/src/VirtualClient/VirtualClient.Actions/Lzbench/lzbenchexecutor.sh @@ -1,5 +1,4 @@ #!/bin/bash -echo $1 -./lzbench $1 > results-summary.csv - +echo "$1" +./lzbench $1 > "$2" diff --git a/src/VirtualClient/VirtualClient.Main/profiles/PERF-COMPRESSION-LZBENCH.json b/src/VirtualClient/VirtualClient.Main/profiles/PERF-COMPRESSION-LZBENCH.json index d8cd3ee786..9c23e3ec27 100644 --- a/src/VirtualClient/VirtualClient.Main/profiles/PERF-COMPRESSION-LZBENCH.json +++ b/src/VirtualClient/VirtualClient.Main/profiles/PERF-COMPRESSION-LZBENCH.json @@ -1,7 +1,7 @@ { "Description": "LZbench Workload", "Metadata": { - "RecommendedMinimumExecutionTime": "18:00:00", + "RecommendedMinimumExecutionTime": "24:00:00", "SupportedPlatforms": "linux-x64,linux-arm64", "SupportedOperatingSystems": "CBL-Mariner,CentOS,Debian,RedHat,Suse,Ubuntu" }, From 4185067c8256cf439dd80418635287e77d766a02 Mon Sep 17 00:00:00 2001 From: AlexWFMS Date: Wed, 22 Jul 2026 16:30:48 -0700 Subject: [PATCH 2/2] Bump version from 3.3.25 to 3.3.26 Signed-off-by: AlexWFMS --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index d9cb810a69..eb47ca73a4 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.3.25 \ No newline at end of file +3.3.26