Public/network/Measure-NetworkLatency.ps1

#Requires -Version 5.1

function Measure-NetworkLatency {
    <#
    .SYNOPSIS
        Measures network latency to one or more hosts with statistical summary.
    .DESCRIPTION
        Sends ICMP echo requests (ping) to target hosts and computes statistics:
        minimum, maximum, average, jitter (standard deviation), and packet loss
        percentage.
 
        Unlike Test-Connection, this function provides a statistical summary
        rather than individual ping results, making it ideal for network
        quality assessment.
    .PARAMETER ComputerName
        One or more target hostnames or IP addresses. Accepts pipeline input.
    .PARAMETER Count
        Number of ICMP echo requests to send per host. Default: 10.
        Valid range: 1-1000.
    .PARAMETER BufferSize
        Size of the ICMP payload in bytes. Default: 32. Valid range: 1-65500.
    .PARAMETER DelayMs
        Delay between pings in milliseconds. Default: 500. Valid range: 0-10000.
    .EXAMPLE
        Measure-NetworkLatency -ComputerName 'gateway.corp.local'
 
        Sends 10 pings and returns latency statistics.
    .EXAMPLE
        Measure-NetworkLatency -ComputerName '8.8.8.8', '1.1.1.1' -Count 50
 
        Compares latency to Google DNS and Cloudflare with 50 pings each.
    .EXAMPLE
        'SRV01', 'SRV02', 'SRV03' | Measure-NetworkLatency -Count 20
 
        Pipeline: measures latency to 3 servers with 20 pings each.
    .OUTPUTS
    PSWinOps.NetworkLatency
    .NOTES
        Author: Franck SALLET
        Version: 1.0.0
        Last Modified: 2026-03-21
        Requires: PowerShell 5.1+ / Windows only
        Permissions: No admin required (ICMP may be blocked by firewall)
    #>

    [CmdletBinding()]
    [OutputType('PSWinOps.NetworkLatency')]
    param (
        [Parameter(Mandatory = $true,
            ValueFromPipeline = $true,
            ValueFromPipelineByPropertyName = $true)]
        [ValidateNotNullOrEmpty()]
        [Alias('CN', 'Name', 'DNSHostName')]
        [string[]]$ComputerName,

        [Parameter(Mandatory = $false)]
        [ValidateRange(1, 1000)]
        [int]$Count = 10,

        [Parameter(Mandatory = $false)]
        [ValidateRange(1, 65500)]
        [int]$BufferSize = 32,

        [Parameter(Mandatory = $false)]
        [ValidateRange(0, 10000)]
        [int]$DelayMs = 500
    )

    begin {
        Write-Verbose "[$($MyInvocation.MyCommand)] Starting latency measurement (Count: $Count, Buffer: $BufferSize bytes)"
    }

    process {
        foreach ($targetComputer in $ComputerName) {
            try {
                Write-Verbose "[$($MyInvocation.MyCommand)] Pinging '$targetComputer' ($Count times)"
                $pingSender = New-Object System.Net.NetworkInformation.Ping
                $buffer = [byte[]]::new($BufferSize)
                $pingOptions = New-Object System.Net.NetworkInformation.PingOptions(128, $true)

                $latencies = [System.Collections.Generic.List[double]]::new()
                $sent = 0
                $received = 0
                $resolvedAddress = $null

                for ($i = 0; $i -lt $Count; $i++) {
                    $sent++
                    try {
                        $reply = $pingSender.Send($targetComputer, 5000, $buffer, $pingOptions)
                        if ($reply.Status -eq [System.Net.NetworkInformation.IPStatus]::Success) {
                            $received++
                            $latencies.Add($reply.RoundtripTime)
                            if (-not $resolvedAddress) {
                                $resolvedAddress = $reply.Address.ToString()
                            }
                        }
                    } catch {
                        Write-Verbose "[$($MyInvocation.MyCommand)] Ping $($i + 1) to '$targetComputer' failed: $_"
                    }

                    if ($i -lt ($Count - 1) -and $DelayMs -gt 0) {
                        Start-Sleep -Milliseconds $DelayMs
                    }
                }

                $pingSender.Dispose()

                # Compute statistics
                $lost = $sent - $received
                $lossPercent = if ($sent -gt 0) { [math]::Round(($lost / $sent) * 100, 1) } else { 100.0 }

                if ($latencies.Count -gt 0) {
                    $minMs = [math]::Round(($latencies | Measure-Object -Minimum).Minimum, 1)
                    $maxMs = [math]::Round(($latencies | Measure-Object -Maximum).Maximum, 1)
                    $avgMs = [math]::Round(($latencies | Measure-Object -Average).Average, 1)

                    # Jitter = standard deviation
                    if ($latencies.Count -gt 1) {
                        $mean = ($latencies | Measure-Object -Average).Average
                        $sumSquares = ($latencies | ForEach-Object { ($_ - $mean) * ($_ - $mean) } | Measure-Object -Sum).Sum
                        $jitterMs = [math]::Round([math]::Sqrt($sumSquares / ($latencies.Count - 1)), 1)
                    } else {
                        $jitterMs = 0.0
                    }
                } else {
                    $minMs = $null
                    $maxMs = $null
                    $avgMs = $null
                    $jitterMs = $null
                }

                [PSCustomObject]@{
                    PSTypeName      = 'PSWinOps.NetworkLatency'
                    ComputerName    = $targetComputer
                    IPAddress       = $resolvedAddress
                    Sent            = $sent
                    Received        = $received
                    Lost            = $lost
                    LossPercent     = $lossPercent
                    MinMs           = $minMs
                    MaxMs           = $maxMs
                    AvgMs           = $avgMs
                    JitterMs        = $jitterMs
                    Timestamp       = Get-Date -Format 'o'
                }
            } catch {
                Write-Error "[$($MyInvocation.MyCommand)] Failed on '$targetComputer': $_"
            }
        }
    }

    end {
        Write-Verbose "[$($MyInvocation.MyCommand)] Completed latency measurement"
    }
}