From 268f1c55904c43773b6d7ae50190d5fb5eb724d2 Mon Sep 17 00:00:00 2001 From: JBg Date: Mon, 17 Mar 2025 13:20:28 +0100 Subject: [PATCH] Fixed memory usage, stop automatically after 24h --- Ping_Monitor.ps1 | 255 +++++++++++++++++++++++++++++++++-------------- 1 file changed, 181 insertions(+), 74 deletions(-) diff --git a/Ping_Monitor.ps1 b/Ping_Monitor.ps1 index 3662d7f..9ec76cc 100644 --- a/Ping_Monitor.ps1 +++ b/Ping_Monitor.ps1 @@ -4,7 +4,6 @@ $addressDefinitions = @( "1.1.1.1", "1.0.0.1", "8.8.8.8-9" # Using range notation for cleaner definition - ) # Set up logging configuration @@ -19,29 +18,51 @@ if (-not (Test-Path $logFolder)) { New-Item -Path $logFolder -ItemType Directory | Out-Null } -# Initialize log files with headers -if (-not (Test-Path $csvLogFile)) { - "Timestamp,IPAddress,Status,ResponseTime,SuccessRate" | Out-File -FilePath $csvLogFile +# Initialize log files with headers - Use streamwriter for lower memory footprint +$null = if (-not (Test-Path $csvLogFile)) { + $csvStream = [System.IO.StreamWriter]::new($csvLogFile) + $csvStream.WriteLine("Timestamp,IPAddress,Status,ResponseTime,SuccessRate") + $csvStream.Close() + $csvStream.Dispose() } -# Function to write to log file with timestamp +# Prepare log streamwriters - more memory efficient than Out-File +$logStream = $null +$statLogStream = $null +$errorLogStream = $null +$csvLogStream = $null + +# Function to write to log file with timestamp - using streamwriter for efficiency function Write-Log { param( [Parameter(Mandatory=$true)] [string]$Message, - [string]$LogFile = $logFile, + [string]$LogPath = $logFile, [switch]$NoConsole ) + $timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss' - "$timestamp - $Message" | Out-File -FilePath $LogFile -Append + $logLine = "$timestamp - $Message" + + # Use streamwriter instead of Out-File + $stream = [System.IO.StreamWriter]::new($LogPath, $true) + $stream.WriteLine($logLine) + $stream.Close() + $stream.Dispose() + if (-not $NoConsole) { - Write-Host "$timestamp - $Message" + Write-Host $logLine } } # Log script start Write-Log "Network monitoring script started" +# Set script end time (24 hours from now) +$endTime = (Get-Date).AddHours(24) +Write-Log "Script will automatically stop at $($endTime.ToString('yyyy-MM-dd HH:mm:ss'))" + +# Process IP address definitions more efficiently foreach ($addr in $addressDefinitions) { if ($addr -match "(\d+\.\d+\.\d+\.)(\d+)-(\d+)") { $prefix = $matches[1] @@ -58,28 +79,43 @@ foreach ($addr in $addressDefinitions) { # Log monitored addresses Write-Log "Monitoring IP addresses: $($addresses -join ', ')" -# Initialize hashtables to store statistics +# Initialize statistics storage - Use simpler data structures for memory efficiency +$sampleSize = 60 # Maximum number of samples to keep $stats = @{} + foreach ($addr in $addresses) { + # Use fixed-size arrays instead of queue for better memory efficiency $stats[$addr] = @{ MinTime = [int]::MaxValue MaxTime = 0 - TotalTime = 0 + SampleArray = New-Object 'int[]' $sampleSize # Fixed-size array + SamplePosition = 0 # Current position in array + SampleCount = 0 # Current number of samples (up to sampleSize) SuccessCount = 0 FailCount = 0 - Samples = [System.Collections.Queue]::new(60) # Fixed-size queue for better memory management LastStatus = $null - StatusChanged = $false LastLoggedStats = [DateTime]::MinValue } } -# Function to calculate mean -function Get-Mean { - param($Numbers) - if ($Numbers.Count -eq 0) { return 0 } - $array = [array]($Numbers) - return ($array | Measure-Object -Average).Average +# More efficient mean calculation for fixed array +function Get-ArrayMean { + param( + [int[]]$Array, + [int]$Count, + [int]$Max + ) + + if ($Count -eq 0) { return 0 } + + $sum = 0 + $actualCount = [Math]::Min($Count, $Max) + + for ($i = 0; $i -lt $actualCount; $i++) { + $sum += $Array[$i] + } + + return $sum / $actualCount } # Function to clear the console and position cursor @@ -88,7 +124,7 @@ function Reset-Display { $host.UI.RawUI.CursorPosition = @{X=0; Y=0} } -# Function to format number with fixed width +# Function to format number with fixed width - optimized to reduce string operations function Format-Number { param( [Parameter(Mandatory=$true)] @@ -96,26 +132,55 @@ function Format-Number { [int]$Width = 6, [int]$Decimals = 1 ) - if ($Number -eq [int]::MaxValue) { return "".PadLeft($Width) } - return $Number.ToString("F$Decimals").PadLeft($Width) + + if ($Number -eq [int]::MaxValue) { + return ' ' * $Width + } + + $formatted = [string]::Format("{0:F$Decimals}", $Number) + $padding = $Width - $formatted.Length + + if ($padding -gt 0) { + return (' ' * $padding) + $formatted + } + return $formatted } -# Function to log status changes +# Function to log status changes - streamwriter based function Write-StatusLog { param( [string]$Address, - [string]$OldStatus, - [string]$NewStatus + [bool]$OldStatus, + [bool]$NewStatus ) - $statusText = if ($NewStatus -eq $true) { "UP" } else { "DOWN" } - $oldStatusText = if ($OldStatus -eq $true) { "UP" } else { "DOWN" } + + $statusText = if ($NewStatus) { "UP" } else { "DOWN" } + $oldStatusText = if ($OldStatus) { "UP" } else { "DOWN" } $logMessage = "$Address status changed from $oldStatusText to $statusText" Write-Log $logMessage Write-Host "`n$logMessage" -ForegroundColor Yellow } -# Function to log statistics periodically +# Function to log ping data to CSV - using streamwriter for efficiency +function Write-PingDataLog { + param( + [string]$Address, + [bool]$Status, + [int]$ResponseTime, + [double]$SuccessRate + ) + + $statusText = if ($Status) { "UP" } else { "DOWN" } + $timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss' + + $stream = [System.IO.StreamWriter]::new($csvLogFile, $true) + $stream.WriteLine("$timestamp,$Address,$statusText,$ResponseTime,$SuccessRate") + $stream.Close() + $stream.Dispose() +} + +# Function to log statistics periodically - streamwriter based function Write-StatisticsLog { param( [string]$Address, @@ -130,34 +195,18 @@ function Write-StatisticsLog { [math]::Round(($Statistics.SuccessCount / $totalRequests) * 100, 1) } else { 0 } - $meanTime = if ($Statistics.Samples.Count -gt 0) { - [math]::Round((Get-Mean $Statistics.Samples), 1) - } else { 0 } + $meanTime = [math]::Round((Get-ArrayMean -Array $Statistics.SampleArray -Count $Statistics.SampleCount -Max $sampleSize), 1) $statsMessage = "$Address Stats - Min: $($Statistics.MinTime)ms, Max: $($Statistics.MaxTime)ms, " + "Mean: ${meanTime}ms, Success Rate: ${successRate}%, " + "Success Count: $($Statistics.SuccessCount), Fail Count: $($Statistics.FailCount)" - Write-Log -Message $statsMessage -LogFile $statLogFile -NoConsole + Write-Log -Message $statsMessage -LogPath $statLogFile -NoConsole $Statistics.LastLoggedStats = $now } } -# Function to log ping data to CSV -function Write-PingDataLog { - param( - [string]$Address, - [bool]$Status, - [int]$ResponseTime, - [double]$SuccessRate - ) - - $statusText = if ($Status) { "UP" } else { "DOWN" } - $timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss' - "$timestamp,$Address,$statusText,$ResponseTime,$SuccessRate" | Out-File -FilePath $csvLogFile -Append -} - -# Function to handle errors +# Function to handle errors - streamwriter based function Write-ErrorLog { param( [Parameter(Mandatory=$true)] @@ -166,26 +215,55 @@ function Write-ErrorLog { ) $errorMessage = "[$Context] $($ErrorRecord.Exception.GetType().Name): $($ErrorRecord.Exception.Message)" - $errorDetails = $ErrorRecord | Format-List * -Force | Out-String - Write-Log -Message $errorMessage -LogFile $errorLogFile -NoConsole - Write-Log -Message "Details: $errorDetails" -LogFile $errorLogFile -NoConsole + # More memory efficient than Format-List + $errorDetails = "Exception: $($ErrorRecord.Exception)" + + Write-Log -Message $errorMessage -LogPath $errorLogFile -NoConsole + Write-Log -Message "Details: $errorDetails" -LogPath $errorLogFile -NoConsole Write-Host "Error: $errorMessage" -ForegroundColor Red } +# Function to display time remaining +function Format-TimeRemaining { + param([TimeSpan]$TimeSpan) + return "{0:D2}:{1:D2}:{2:D2}" -f $TimeSpan.Hours, $TimeSpan.Minutes, $TimeSpan.Seconds +} + +# Memory cleanup function - call periodically +function Invoke-MemoryCleanup { + [System.GC]::Collect() + [System.GC]::WaitForPendingFinalizers() + [System.GC]::Collect() +} + # Main monitoring loop try { $pingInterval = 1 # seconds between pings $statsLogInterval = 15 # minutes between logging statistics $csvLogFrequency = 5 # log every Nth ping to CSV to avoid huge files + $memoryCleanupInterval = 100 # Garbage collect every N pings $pingCounter = 0 + # Create ping object once, outside the loop + $ping = New-Object System.Net.NetworkInformation.Ping + Write-Log "Monitoring started with ping interval of ${pingInterval}s and stats logging every ${statsLogInterval} minutes" while ($true) { + # Check if 24 hours have elapsed + $currentTime = Get-Date + if ($currentTime -ge $endTime) { + Write-Log "24-hour monitoring period completed. Stopping the script." + break + } + + $timeRemaining = $endTime - $currentTime + Reset-Display - $currentTime = Get-Date -Format "HH:mm:ss" - Write-Host "Network Monitor - Last Updated: $currentTime" -ForegroundColor Cyan + $currentTimeFormatted = $currentTime.ToString("HH:mm:ss") + Write-Host "Network Monitor - Last Updated: $currentTimeFormatted" -ForegroundColor Cyan + Write-Host "Time remaining: $(Format-TimeRemaining -TimeSpan $timeRemaining)" -ForegroundColor Cyan Write-Host "Press Ctrl+C to exit`n" -ForegroundColor DarkGray # Header with fixed widths @@ -194,8 +272,12 @@ try { $pingCounter++ + # Force garbage collection periodically + if ($pingCounter % $memoryCleanupInterval -eq 0) { + Invoke-MemoryCleanup + } + foreach ($addr in $addresses) { - $ping = New-Object System.Net.NetworkInformation.Ping try { $result = $ping.Send($addr, 1000) # 1 second timeout $status = $result.Status -eq 'Success' @@ -207,18 +289,23 @@ try { } if ($status) { - $stats[$addr].TotalTime += $responseTime $stats[$addr].SuccessCount++ - # Update rolling queue of samples - suppress output - if ($stats[$addr].Samples.Count -eq 60) { - [void]$stats[$addr].Samples.Dequeue() + # Update sample array using fixed position tracking + $position = $stats[$addr].SamplePosition + $stats[$addr].SampleArray[$position] = $responseTime + $stats[$addr].SamplePosition = ($position + 1) % $sampleSize + if ($stats[$addr].SampleCount -lt $sampleSize) { + $stats[$addr].SampleCount++ } - [void]$stats[$addr].Samples.Enqueue($responseTime) # Update min/max - $stats[$addr].MinTime = [Math]::Min($stats[$addr].MinTime, $responseTime) - $stats[$addr].MaxTime = [Math]::Max($stats[$addr].MaxTime, $responseTime) + if ($stats[$addr].MinTime -eq [int]::MaxValue -or $responseTime -lt $stats[$addr].MinTime) { + $stats[$addr].MinTime = $responseTime + } + if ($responseTime -gt $stats[$addr].MaxTime) { + $stats[$addr].MaxTime = $responseTime + } $statusText = "UP" $color = "Green" @@ -233,9 +320,8 @@ try { $successRate = if ($totalRequests -gt 0) { [math]::Round(($stats[$addr].SuccessCount / $totalRequests) * 100, 1) } else { 0 } - $meanTime = if ($stats[$addr].Samples.Count -gt 0) { - [math]::Round((Get-Mean $stats[$addr].Samples), 1) - } else { 0 } + + $meanTime = [math]::Round((Get-ArrayMean -Array $stats[$addr].SampleArray -Count $stats[$addr].SampleCount -Max $sampleSize), 1) # Check for status changes and log them if ($null -ne $stats[$addr].LastStatus -and $stats[$addr].LastStatus -ne $status) { @@ -251,19 +337,32 @@ try { Write-PingDataLog -Address $addr -Status $status -ResponseTime $responseTime -SuccessRate $successRate } - # Calculate trend based on last 5 samples - suppress output - $trend = if ($stats[$addr].Samples.Count -ge 5) { - $recent = @([array]($stats[$addr].Samples)) - $lastFive = $recent[-[Math]::Min(5, $recent.Count)..-1] - if ($lastFive.Count -ge 2) { - $slope = ($lastFive[-1] - $lastFive[0]) / [Math]::Max(1, ($lastFive.Count - 1)) - if ([Math]::Abs($slope) -lt 1) { "━" } - elseif ($slope -lt 0) { "↓" } - else { "↑" } - } else { "━" } - } else { "━" } + # Calculate trend more efficiently + $trend = "━" # Default to neutral + if ($stats[$addr].SampleCount -ge 5) { + $samples = $stats[$addr].SampleArray + $count = $stats[$addr].SampleCount + $position = $stats[$addr].SamplePosition + + # Get first sample (oldest of last 5) + $firstIndex = ($position - 5 + $sampleSize) % $sampleSize + $first = $samples[$firstIndex] + + # Get last sample (newest) + $lastIndex = ($position - 1 + $sampleSize) % $sampleSize + $last = $samples[$lastIndex] + + $slope = $last - $first + if ([Math]::Abs($slope) -lt 5) { + $trend = "━" + } elseif ($slope -lt 0) { + $trend = "↓" + } else { + $trend = "↑" + } + } - # Format output fields + # Format output fields - efficient string formatting $ipField = $addr.PadRight(15) $statusField = $statusText.PadRight(8) $minField = Format-Number -Number $stats[$addr].MinTime -Width 5 -Decimals 0 @@ -284,8 +383,16 @@ try { # Log any unhandled exceptions Write-ErrorLog -ErrorRecord $_ -Context "Main monitoring loop" } finally { + # Final garbage collection and cleanup + Invoke-MemoryCleanup + # Cleanup when script is interrupted $endMessage = "Monitoring stopped at $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" Write-Log $endMessage Write-Host "`n$endMessage" -ForegroundColor Cyan + + # Dispose of ping object + if ($ping -ne $null) { + $ping.Dispose() + } } \ No newline at end of file