diff --git a/src/NBomber/Plugins/PsPingPlugin.fs b/src/NBomber/Plugins/PsPingPlugin.fs index 504d6e28..1a73bedf 100644 --- a/src/NBomber/Plugins/PsPingPlugin.fs +++ b/src/NBomber/Plugins/PsPingPlugin.fs @@ -10,7 +10,9 @@ open FSharp.Control.Tasks.NonAffine open FsToolkit.ErrorHandling open Microsoft.Extensions.Configuration +open NBomber open NBomber.Contracts +open NBomber.Domain.Stats.Statistics open NBomber.Extensions.InternalExtensions [] @@ -18,19 +20,30 @@ type PsPingPluginConfig = { Hosts: Uri[] /// The default is 1000 ms. Timeout: int + /// Number of warm up ping executions. The default is 1. + WarmUpExecutions: int + /// Number of aggregated ping executions. The default is 4. + Executions: int } with - static member CreateDefault([]hosts: string[]) = { + static member CreateDefault([]hosts: string[]) = + { Hosts = hosts |> Array.map Uri Timeout = 1_000 - } + WarmUpExecutions = 1 + Executions = 4 + } static member CreateDefault(hosts: string seq) = hosts |> Seq.toArray |> PsPingPluginConfig.CreateDefault type PsPingReply = { - Status: string Address: Uri - RoundtripTime: int64 + RequestCountSucceeded: int + RequestCountFailed: int + RoundtripTimeMin: int64 + RoundtripTimeMax: int64 + RoundtripTimeAvg: float + RoundtripTimeStdDev: float } module internal PsPingPluginStatistics = @@ -43,8 +56,8 @@ module internal PsPingPluginStatistics = let private createColumns () = [| "Host", "Host", "System.String" "Port", "Port", "System.Int32" - "Status", "Status", "System.String" "Address", "Address", "System.String" + "Requests", "Requests", "System.String" "RoundTripTime", "Round Trip Time", "System.String" |] |> Array.map(fun x -> x |> createColumn) @@ -53,9 +66,13 @@ module internal PsPingPluginStatistics = row.["Host"] <- host row.["Port"] <- port - row.["Status"] <- pingReply.Status.ToString() row.["Address"] <- pingReply.Address.ToString() - row.["RoundTripTime"] <- $"%i{pingReply.RoundtripTime} ms" + row.["Requests"] <- $"all = {pingReply.RequestCountSucceeded + pingReply.RequestCountFailed}, ok = {pingReply.RequestCountSucceeded}, fail = {pingReply.RequestCountFailed}" + row.["RoundTripTime"] <- + $"min = {pingReply.RoundtripTimeMin}" + + $", mean = {pingReply.RoundtripTimeAvg |> Converter.round(Constants.StatsRounding)}" + + $", max = {pingReply.RoundtripTimeMax}" + + $", StdDev = {pingReply.RoundtripTimeStdDev |> Converter.round(Constants.StatsRounding)}" row @@ -76,10 +93,10 @@ module internal PsPingPluginHintsAnalyzer = let analyze (pingResults: (string * int * PsPingReply)[]) = let printHint (hostName, port, result: PsPingReply) = - $"Physical latency to host: '%s{hostName}' on port: '%i{port}' is '%d{result.RoundtripTime}'. This is bigger than 2ms which is not appropriate for load testing. You should run your test in an environment with very small latency." + $"Physical latency to host: '%s{hostName}' on port: '%i{port}' is '%f{result.RoundtripTimeAvg}'. This is bigger than 2ms which is not appropriate for load testing. You should run your test in an environment with very small latency." pingResults - |> Seq.filter(fun (_,_,result) -> result.RoundtripTime > 2L) + |> Seq.filter(fun (_,_,result) -> result.RoundtripTimeAvg > 2.0) |> Seq.map printHint |> Seq.toArray @@ -92,33 +109,52 @@ type PsPingPlugin(pluginConfig: PsPingPluginConfig) = let execPing (config: PsPingPluginConfig) = task { try - // from https://stackoverflow.com/questions/26067342/how-to-implement-psping-tcp-ping-in-c-sharp - use sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp) - sock.Blocking <- true - - let! replies = + let replies = config.Hosts - |> Array.map(fun uri -> task { - let stopwatch = Stopwatch() + |> Array.map(fun uri -> + let results = + [1..config.WarmUpExecutions + config.Executions] + |> List.map(fun _ -> + // from https://stackoverflow.com/questions/26067342/how-to-implement-psping-tcp-ping-in-c-sharp + use sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp) + sock.Blocking <- true + + let stopwatch = Stopwatch() + + // Measure the Connect call only + stopwatch.Start() + let connectTask = sock.ConnectAsync(uri.Host, uri.Port) + let _ = connectTask.Wait(config.Timeout) // we do not care if it completed OK or not, as the result will get anyway sock.Connected property ... + stopwatch.Stop() - // measure the Connect call only - stopwatch.Start() - let connectTask = sock.ConnectAsync(uri.Host, uri.Port) - let timeoutTask = Task.Delay(config.Timeout) - do! Task.WhenAny(connectTask, timeoutTask) |> Task.map ignore - stopwatch.Stop() + let result = + sock.Connected, + stopwatch.Elapsed.TotalMilliseconds |> int64 + sock.Close() + + System.Threading.Thread.Sleep(500) // to have some interval between running the tasks + + result + ) + + let results = results |> Seq.skip config.WarmUpExecutions + let totalMsResults = results |> Seq.map (fun (_, totalMs) -> totalMs |> float) + let avg = totalMsResults |> Seq.average let psPingReply = { - Status = if sock.Connected then "Connected" else "NotConnected/TimedOut" Address = uri - RoundtripTime = stopwatch.Elapsed.TotalMilliseconds |> int64 + RequestCountSucceeded = results |> Seq.filter (fun (connected, _) -> connected) |> Seq.length + RequestCountFailed = results |> Seq.filter (fun (connected, _) -> not connected) |> Seq.length + RoundtripTimeMin = totalMsResults |> Seq.min |> int64 + RoundtripTimeMax = totalMsResults |> Seq.max |> int64 + RoundtripTimeAvg = avg + RoundtripTimeStdDev = + let sumOfSquaresOfDifferences = totalMsResults |> Seq.map (fun totalMs -> (totalMs - avg) * (totalMs - avg)) |> Seq.sum + Math.Sqrt(sumOfSquaresOfDifferences / (totalMsResults |> Seq.length |> float)) } - return uri.Host, uri.Port, psPingReply - }) - |> Task.WhenAll - - sock.Close() + uri.Host, uri.Port, psPingReply + ) return Ok replies with @@ -145,20 +181,20 @@ type PsPingPlugin(pluginConfig: PsPingPluginConfig) = _logger <- context.Logger.ForContext() let config = - infraConfig.GetSection("PsPingPlugin").Get() + infraConfig.GetSection("PingPlugin").Get() |> Option.ofRecord |> Option.defaultValue pluginConfig - _logger.Verbose("PsPingPlugin config: @{PsPingPluginConfig}", config) + _logger.Verbose("PingPlugin config: @{PingPluginConfig}", config) config |> execPing - |> Task.map(createStats config) - |> Task.map(Result.map(fun (pingResults,stats) -> + |> Task.map (createStats config) + |> Task.map (Result.map(fun (pingResults,stats) -> _pingResults <- pingResults _pluginStats <- stats )) - |> Task.map(Result.mapError(fun ex -> _logger.Error(ex.ToString()))) + |> Task.map (Result.mapError(fun ex -> _logger.Error(ex.ToString()))) |> Task.map ignore :> Task