From d2eabbed8628ff4eee4892c7ae0ce1dc6b21b9fb Mon Sep 17 00:00:00 2001 From: Rudra Pratap SIngh Date: Tue, 14 Oct 2025 16:36:15 +0530 Subject: [PATCH 1/2] Add duration parameter to network ping workload --- VERSION | 2 +- .../Network/NetworkPingExecutor.cs | 32 +++++++++++++++---- .../profiles/PERF-NETWORK-PING.json | 4 ++- .../network-ping/network-ping-profiles.md | 16 +++++++--- 4 files changed, 41 insertions(+), 13 deletions(-) diff --git a/VERSION b/VERSION index e3baf30b73..0aa25e148f 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.1.35 \ No newline at end of file +2.1.36 \ No newline at end of file diff --git a/src/VirtualClient/VirtualClient.Actions/Network/NetworkPingExecutor.cs b/src/VirtualClient/VirtualClient.Actions/Network/NetworkPingExecutor.cs index 27a6c5a39b..33805e5e05 100644 --- a/src/VirtualClient/VirtualClient.Actions/Network/NetworkPingExecutor.cs +++ b/src/VirtualClient/VirtualClient.Actions/Network/NetworkPingExecutor.cs @@ -64,6 +64,18 @@ public int PingIterations } } + /// + /// Parameter. Defines the duration for which the network ping test will run. + /// This can be a valid timespan (e.g. 00:10:00) or a simple numeric value representing total seconds (e.g. 600). + /// + public TimeSpan Duration + { + get + { + return this.Parameters.GetTimeSpanValue(nameof(NetworkPingExecutor.Duration), TimeSpan.Zero); + } + } + /// /// The retry policy to apply to ping operations for handling transient /// issues/errors. @@ -116,7 +128,13 @@ private async Task ExecutePingServerAsync(IPAddress ipAddress, EventContext tele DateTime startTime = DateTime.UtcNow; Stopwatch blipTimer = Stopwatch.StartNew(); - while (iterations < this.PingIterations && !cancellationToken.IsCancellationRequested) + + // Use Duration if specified, otherwise use PingIterations + bool useDuration = this.Duration > TimeSpan.Zero; + DateTime endTime = useDuration ? startTime.Add(this.Duration) : DateTime.MaxValue; + + while (!cancellationToken.IsCancellationRequested && + (useDuration ? DateTime.UtcNow < endTime : iterations < this.PingIterations)) { try { @@ -168,7 +186,7 @@ await this.PingRetryPolicy.ExecuteAsync(async () => } } - DateTime endTime = DateTime.UtcNow; + DateTime metricsEndTime = DateTime.UtcNow; if (!cancellationToken.IsCancellationRequested) { @@ -185,7 +203,7 @@ await this.PingRetryPolicy.ExecuteAsync(async () => "NetworkPing", "Network Ping", startTime, - endTime, + metricsEndTime, "avg. round trip time", responseTimes.Average(), MetricUnit.Milliseconds, @@ -204,7 +222,7 @@ await this.PingRetryPolicy.ExecuteAsync(async () => "NetworkPing", "Network Ping", startTime, - endTime, + metricsEndTime, "avg. number of connections", networkConnections.Average(), "count", @@ -223,7 +241,7 @@ await this.PingRetryPolicy.ExecuteAsync(async () => "NetworkPing", "Network Ping", startTime, - endTime, + metricsEndTime, "# of blips", networkBlips.Count, "count", @@ -237,7 +255,7 @@ await this.PingRetryPolicy.ExecuteAsync(async () => "NetworkPing", "Network Ping", startTime, - endTime, + metricsEndTime, "dropped pings", networkBlips.Select(blip => blip.DroppedAttempts).Aggregate((total, attempts) => total += attempts), "count", @@ -253,7 +271,7 @@ await this.PingRetryPolicy.ExecuteAsync(async () => "NetworkPing", "Network Ping", startTime, - endTime, + metricsEndTime, "blip duration", blip.Duration, MetricUnit.Milliseconds, diff --git a/src/VirtualClient/VirtualClient.Main/profiles/PERF-NETWORK-PING.json b/src/VirtualClient/VirtualClient.Main/profiles/PERF-NETWORK-PING.json index a3a5ede5cd..8c77de864f 100644 --- a/src/VirtualClient/VirtualClient.Main/profiles/PERF-NETWORK-PING.json +++ b/src/VirtualClient/VirtualClient.Main/profiles/PERF-NETWORK-PING.json @@ -6,7 +6,8 @@ "SupportedOperatingSystems": "CBL-Mariner,CentOS,Debian,RedHat,Suse,Ubuntu,Windows" }, "Parameters": { - "IPAddress": "NotDefined" + "IPAddress": "NotDefined", + "Duration": "00:00:00" }, "Actions": [ { @@ -14,6 +15,7 @@ "Parameters": { "Scenario": "ICMP", "IPAddress": "$.Parameters.IPAddress", + "Duration": "$.Parameters.Duration", "PingIterations": 300, "Tags": "Performance,Networking,Ping" } diff --git a/website/docs/workloads/network-ping/network-ping-profiles.md b/website/docs/workloads/network-ping/network-ping-profiles.md index b3328bc2b7..c0b1b0b3f5 100644 --- a/website/docs/workloads/network-ping/network-ping-profiles.md +++ b/website/docs/workloads/network-ping/network-ping-profiles.md @@ -30,9 +30,11 @@ respond to network ping requests. * **Profile Parameters** The following parameters can be optionally supplied on the command line to modify the behaviors of the workload. - | Parameter | Purpose | - |-------------|---------| - | IPAddress | Required. The IP address of the target endpoint to which to send network pings and measure round trip response times. Loopback address can be used: 127.0.0.1. | + | Parameter | Purpose | Default Value | + |----------------|---------|---------------| + | IPAddress | Required. The IP address of the target endpoint to which to send network pings and measure round trip response times. Loopback address can be used: 127.0.0.1. | NotDefined | + | Duration | Optional. The duration for which the network ping test will run. This can be a valid timespan (e.g. 00:10:00 for 10 minutes) or a simple numeric value representing total seconds (e.g. 600). When specified, it overrides PingIterations. | Not specified (uses PingIterations) | + | PingIterations | Optional. The number of individual network pings that will be conducted. This is used when Duration is not specified. | 300 | * **Profile Runtimes** See the 'Metadata' section of the profile for estimated runtimes. These timings represent the length of time required to run a single round of profile @@ -43,6 +45,12 @@ respond to network ping requests. The following section provides a few basic examples of how to use the workload profile. ``` bash - # Execute the workload profile + # Execute the workload profile with default behavior (300 ping iterations) VirtualClient.exe --profile=PERF-NETWORK-PING.json --system=Demo --timeout=1440 --parameters=IPAddress=1.2.3.4 + + # Execute the workload profile with custom duration (10 minutes) + VirtualClient.exe --profile=PERF-NETWORK-PING.json --system=Demo --timeout=1440 --parameters=IPAddress=1.2.3.4,,,Duration=00:10:00 + + # Execute the workload profile with custom ping iterations + VirtualClient.exe --profile=PERF-NETWORK-PING.json --system=Demo --timeout=1440 --parameters=IPAddress=1.2.3.4,,,PingIterations=500 ``` \ No newline at end of file From 948637ea6f5733a4aceef0bae02a4638b58c4d75 Mon Sep 17 00:00:00 2001 From: Rudra Pratap SIngh Date: Wed, 15 Oct 2025 09:44:21 +0530 Subject: [PATCH 2/2] Address comments --- .../Network/NetworkPingExecutorTests.cs | 251 ++++++++++++++++++ .../Network/NetworkPingExecutor.cs | 38 ++- .../profiles/PERF-NETWORK-PING.json | 5 +- .../network-ping/network-ping-profiles.md | 2 +- 4 files changed, 281 insertions(+), 15 deletions(-) create mode 100644 src/VirtualClient/VirtualClient.Actions.UnitTests/Network/NetworkPingExecutorTests.cs diff --git a/src/VirtualClient/VirtualClient.Actions.UnitTests/Network/NetworkPingExecutorTests.cs b/src/VirtualClient/VirtualClient.Actions.UnitTests/Network/NetworkPingExecutorTests.cs new file mode 100644 index 0000000000..7d64084f05 --- /dev/null +++ b/src/VirtualClient/VirtualClient.Actions.UnitTests/Network/NetworkPingExecutorTests.cs @@ -0,0 +1,251 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace VirtualClient.Actions +{ + using System; + using System.Collections.Generic; + using System.Threading; + using System.Threading.Tasks; + using NUnit.Framework; + using VirtualClient.Actions.NetworkPerformance; + using VirtualClient.Contracts; + + [TestFixture] + [Category("Unit")] + public class NetworkPingExecutorTests + { + private MockFixture mockFixture; + + [SetUp] + public void SetupTest() + { + this.mockFixture = new MockFixture(); + } + + [Test] + public void NetworkPingExecutorThrowsIfIPAddressIsNotDefined() + { + this.mockFixture.Parameters = new Dictionary + { + { nameof(NetworkPingExecutor.IPAddress), "NotDefined" } + }; + + using (NetworkPingExecutor executor = new NetworkPingExecutor(this.mockFixture.Dependencies, this.mockFixture.Parameters)) + { + WorkloadException exception = Assert.ThrowsAsync( + () => executor.ExecuteAsync(CancellationToken.None)); + + Assert.AreEqual(ErrorReason.InstructionsNotProvided, exception.Reason); + Assert.IsTrue(exception.Message.Contains("IP address")); + } + } + + [Test] + [TestCase("")] + [TestCase("invalid-ip")] + [TestCase("999.999.999.999")] + [TestCase("not.an.ip.address")] + public void NetworkPingExecutorThrowsIfIPAddressIsInvalid(string invalidIpAddress) + { + this.mockFixture.Parameters = new Dictionary + { + { nameof(NetworkPingExecutor.IPAddress), invalidIpAddress } + }; + + using (NetworkPingExecutor executor = new NetworkPingExecutor(this.mockFixture.Dependencies, this.mockFixture.Parameters)) + { + WorkloadException exception = Assert.ThrowsAsync( + () => executor.ExecuteAsync(CancellationToken.None)); + + Assert.AreEqual(ErrorReason.InstructionsNotValid, exception.Reason); + Assert.IsTrue(exception.Message.Contains("Invalid IP address format")); + } + } + + [Test] + public void NetworkPingExecutorParsesIPAddressParameter() + { + this.mockFixture.Parameters = new Dictionary + { + { nameof(NetworkPingExecutor.IPAddress), "192.168.1.1" } + }; + + using (NetworkPingExecutor executor = new NetworkPingExecutor(this.mockFixture.Dependencies, this.mockFixture.Parameters)) + { + Assert.AreEqual("192.168.1.1", executor.IPAddress); + } + } + + [Test] + [TestCase("192.168.1.1")] + [TestCase("10.0.0.1")] + [TestCase("2001:0db8:85a3:0000:0000:8a2e:0370:7334")] + [TestCase("::1")] + public void NetworkPingExecutorAcceptsValidIPAddresses(string ipAddress) + { + this.mockFixture.Parameters = new Dictionary + { + { nameof(NetworkPingExecutor.IPAddress), ipAddress } + }; + + using (NetworkPingExecutor executor = new NetworkPingExecutor(this.mockFixture.Dependencies, this.mockFixture.Parameters)) + { + Assert.AreEqual(ipAddress, executor.IPAddress); + } + } + + [Test] + public void NetworkPingExecutorUsesDefaultPingIterationsWhenNotSpecified() + { + this.mockFixture.Parameters = new Dictionary + { + { nameof(NetworkPingExecutor.IPAddress), "192.168.1.1" } + }; + + using (NetworkPingExecutor executor = new NetworkPingExecutor(this.mockFixture.Dependencies, this.mockFixture.Parameters)) + { + Assert.AreEqual(50, executor.PingIterations); + } + } + + [Test] + [TestCase(100)] + [TestCase(200)] + [TestCase(500)] + public void NetworkPingExecutorParsesPingIterationsParameter(int iterations) + { + this.mockFixture.Parameters = new Dictionary + { + { nameof(NetworkPingExecutor.IPAddress), "192.168.1.1" }, + { nameof(NetworkPingExecutor.PingIterations), iterations } + }; + + using (NetworkPingExecutor executor = new NetworkPingExecutor(this.mockFixture.Dependencies, this.mockFixture.Parameters)) + { + Assert.AreEqual(iterations, executor.PingIterations); + } + } + + [Test] + public void NetworkPingExecutorDurationIsNullWhenNotSpecified() + { + this.mockFixture.Parameters = new Dictionary + { + { nameof(NetworkPingExecutor.IPAddress), "192.168.1.1" } + }; + + using (NetworkPingExecutor executor = new NetworkPingExecutor(this.mockFixture.Dependencies, this.mockFixture.Parameters)) + { + Assert.IsNull(executor.Duration); + } + } + + [Test] + public void NetworkPingExecutorDurationIsNullWhenSetToEmptyString() + { + this.mockFixture.Parameters = new Dictionary + { + { nameof(NetworkPingExecutor.IPAddress), "192.168.1.1" }, + { nameof(NetworkPingExecutor.Duration), string.Empty } + }; + + using (NetworkPingExecutor executor = new NetworkPingExecutor(this.mockFixture.Dependencies, this.mockFixture.Parameters)) + { + Assert.IsNull(executor.Duration); + } + } + + [Test] + public void NetworkPingExecutorDurationIsNullWhenSetToNull() + { + this.mockFixture.Parameters = new Dictionary + { + { nameof(NetworkPingExecutor.IPAddress), "192.168.1.1" }, + { nameof(NetworkPingExecutor.Duration), null } + }; + + using (NetworkPingExecutor executor = new NetworkPingExecutor(this.mockFixture.Dependencies, this.mockFixture.Parameters)) + { + Assert.IsNull(executor.Duration); + } + } + + [Test] + [TestCase("00:05:00", 300)] // 5 minutes = 300 seconds + [TestCase("00:10:00", 600)] // 10 minutes = 600 seconds + [TestCase("01:00:00", 3600)] // 1 hour = 3600 seconds + [TestCase("00:00:30", 30)] // 30 seconds + public void NetworkPingExecutorParsesTimeSpanFormatForDuration(string durationString, int expectedSeconds) + { + this.mockFixture.Parameters = new Dictionary + { + { nameof(NetworkPingExecutor.IPAddress), "192.168.1.1" }, + { nameof(NetworkPingExecutor.Duration), durationString } + }; + + using (NetworkPingExecutor executor = new NetworkPingExecutor(this.mockFixture.Dependencies, this.mockFixture.Parameters)) + { + Assert.IsNotNull(executor.Duration); + Assert.AreEqual(expectedSeconds, executor.Duration.Value.TotalSeconds); + } + } + + [Test] + [TestCase(300)] // 300 seconds + [TestCase(600)] // 600 seconds + [TestCase(3600)] // 3600 seconds + public void NetworkPingExecutorParsesNumericFormatForDuration(int durationSeconds) + { + this.mockFixture.Parameters = new Dictionary + { + { nameof(NetworkPingExecutor.IPAddress), "192.168.1.1" }, + { nameof(NetworkPingExecutor.Duration), durationSeconds } + }; + + using (NetworkPingExecutor executor = new NetworkPingExecutor(this.mockFixture.Dependencies, this.mockFixture.Parameters)) + { + Assert.IsNotNull(executor.Duration); + Assert.AreEqual(durationSeconds, executor.Duration.Value.TotalSeconds); + } + } + + [Test] + public void NetworkPingExecutorSupportsSettingBothDurationAndPingIterations() + { + // When Duration is set, it takes precedence over PingIterations + this.mockFixture.Parameters = new Dictionary + { + { nameof(NetworkPingExecutor.IPAddress), "192.168.1.1" }, + { nameof(NetworkPingExecutor.Duration), "00:05:00" }, + { nameof(NetworkPingExecutor.PingIterations), 100 } + }; + + using (NetworkPingExecutor executor = new NetworkPingExecutor(this.mockFixture.Dependencies, this.mockFixture.Parameters)) + { + Assert.IsNotNull(executor.Duration); + Assert.AreEqual(300, executor.Duration.Value.TotalSeconds); + Assert.AreEqual(100, executor.PingIterations); + } + } + + [Test] + public void NetworkPingExecutorParametersAreConsistentWithProfile() + { + // Verify that the parameters match what's expected in the PERF-NETWORK-PING.json profile + this.mockFixture.Parameters = new Dictionary + { + { nameof(NetworkPingExecutor.IPAddress), "192.168.1.1" }, + { nameof(NetworkPingExecutor.Duration), null }, + { nameof(NetworkPingExecutor.PingIterations), 300 } + }; + + using (NetworkPingExecutor executor = new NetworkPingExecutor(this.mockFixture.Dependencies, this.mockFixture.Parameters)) + { + Assert.AreEqual("192.168.1.1", executor.IPAddress); + Assert.IsNull(executor.Duration); + Assert.AreEqual(300, executor.PingIterations); + } + } + } +} diff --git a/src/VirtualClient/VirtualClient.Actions/Network/NetworkPingExecutor.cs b/src/VirtualClient/VirtualClient.Actions/Network/NetworkPingExecutor.cs index 33805e5e05..248fe205ea 100644 --- a/src/VirtualClient/VirtualClient.Actions/Network/NetworkPingExecutor.cs +++ b/src/VirtualClient/VirtualClient.Actions/Network/NetworkPingExecutor.cs @@ -68,11 +68,21 @@ public int PingIterations /// Parameter. Defines the duration for which the network ping test will run. /// This can be a valid timespan (e.g. 00:10:00) or a simple numeric value representing total seconds (e.g. 600). /// - public TimeSpan Duration + public TimeSpan? Duration { get { - return this.Parameters.GetTimeSpanValue(nameof(NetworkPingExecutor.Duration), TimeSpan.Zero); + // Check if the parameter exists and is not empty + if (this.Parameters.ContainsKey(nameof(NetworkPingExecutor.Duration))) + { + string durationValue = this.Parameters[nameof(NetworkPingExecutor.Duration)]?.ToString(); + if (!string.IsNullOrWhiteSpace(durationValue)) + { + return this.Parameters.GetTimeSpanValue(nameof(NetworkPingExecutor.Duration)); + } + } + + return null; } } @@ -130,12 +140,16 @@ private async Task ExecutePingServerAsync(IPAddress ipAddress, EventContext tele Stopwatch blipTimer = Stopwatch.StartNew(); // Use Duration if specified, otherwise use PingIterations - bool useDuration = this.Duration > TimeSpan.Zero; - DateTime endTime = useDuration ? startTime.Add(this.Duration) : DateTime.MaxValue; + DateTime stopTime = startTime.Add(this.Duration ?? TimeSpan.Zero); - while (!cancellationToken.IsCancellationRequested && - (useDuration ? DateTime.UtcNow < endTime : iterations < this.PingIterations)) + while (!cancellationToken.IsCancellationRequested) { + if ((this.Duration != null && DateTime.UtcNow >= stopTime) || + (this.Duration == null && iterations >= this.PingIterations)) + { + break; + } + try { await this.PingRetryPolicy.ExecuteAsync(async () => @@ -186,7 +200,7 @@ await this.PingRetryPolicy.ExecuteAsync(async () => } } - DateTime metricsEndTime = DateTime.UtcNow; + DateTime endTime = DateTime.UtcNow; if (!cancellationToken.IsCancellationRequested) { @@ -203,7 +217,7 @@ await this.PingRetryPolicy.ExecuteAsync(async () => "NetworkPing", "Network Ping", startTime, - metricsEndTime, + endTime, "avg. round trip time", responseTimes.Average(), MetricUnit.Milliseconds, @@ -222,7 +236,7 @@ await this.PingRetryPolicy.ExecuteAsync(async () => "NetworkPing", "Network Ping", startTime, - metricsEndTime, + endTime, "avg. number of connections", networkConnections.Average(), "count", @@ -241,7 +255,7 @@ await this.PingRetryPolicy.ExecuteAsync(async () => "NetworkPing", "Network Ping", startTime, - metricsEndTime, + endTime, "# of blips", networkBlips.Count, "count", @@ -255,7 +269,7 @@ await this.PingRetryPolicy.ExecuteAsync(async () => "NetworkPing", "Network Ping", startTime, - metricsEndTime, + endTime, "dropped pings", networkBlips.Select(blip => blip.DroppedAttempts).Aggregate((total, attempts) => total += attempts), "count", @@ -271,7 +285,7 @@ await this.PingRetryPolicy.ExecuteAsync(async () => "NetworkPing", "Network Ping", startTime, - metricsEndTime, + endTime, "blip duration", blip.Duration, MetricUnit.Milliseconds, diff --git a/src/VirtualClient/VirtualClient.Main/profiles/PERF-NETWORK-PING.json b/src/VirtualClient/VirtualClient.Main/profiles/PERF-NETWORK-PING.json index 8c77de864f..060ebaf797 100644 --- a/src/VirtualClient/VirtualClient.Main/profiles/PERF-NETWORK-PING.json +++ b/src/VirtualClient/VirtualClient.Main/profiles/PERF-NETWORK-PING.json @@ -7,7 +7,8 @@ }, "Parameters": { "IPAddress": "NotDefined", - "Duration": "00:00:00" + "Duration": null, + "PingIterations": 300 }, "Actions": [ { @@ -16,7 +17,7 @@ "Scenario": "ICMP", "IPAddress": "$.Parameters.IPAddress", "Duration": "$.Parameters.Duration", - "PingIterations": 300, + "PingIterations": "$.Parameters.PingIterations", "Tags": "Performance,Networking,Ping" } } diff --git a/website/docs/workloads/network-ping/network-ping-profiles.md b/website/docs/workloads/network-ping/network-ping-profiles.md index c0b1b0b3f5..26391c0a36 100644 --- a/website/docs/workloads/network-ping/network-ping-profiles.md +++ b/website/docs/workloads/network-ping/network-ping-profiles.md @@ -33,7 +33,7 @@ respond to network ping requests. | Parameter | Purpose | Default Value | |----------------|---------|---------------| | IPAddress | Required. The IP address of the target endpoint to which to send network pings and measure round trip response times. Loopback address can be used: 127.0.0.1. | NotDefined | - | Duration | Optional. The duration for which the network ping test will run. This can be a valid timespan (e.g. 00:10:00 for 10 minutes) or a simple numeric value representing total seconds (e.g. 600). When specified, it overrides PingIterations. | Not specified (uses PingIterations) | + | Duration | Optional. The duration for which the network ping test will run. This can be a valid timespan (e.g. 00:10:00 for 10 minutes) or a simple numeric value representing total seconds (e.g. 600). When specified, it overrides PingIterations. | null (uses PingIterations) | | PingIterations | Optional. The number of individual network pings that will be conducted. This is used when Duration is not specified. | 300 | * **Profile Runtimes**