Functions/Private/Merge-HvDRSTrendSnapshot.ps1
|
function Merge-HvDRSTrendSnapshot { <# .SYNOPSIS Smooths a cluster snapshot's load-sensitive fields over a rolling window of recent passes, so a single transient spike doesn't trigger a migration. .DESCRIPTION Persists a bounded history of prior snapshots' smoothable fields to a JSON file at -HistoryPath, appends the current snapshot, trims to -WindowSize entries, and returns a new snapshot where Node.CpuUtilization, Node.NetworkUtilization, VM.CpuUtilization, and VM.MemoryPressure are replaced by their average over the window. Capacity fields (TotalMemoryMB, AvailableMemoryMB, LogicalProcessorCount) and identity fields are passed through unchanged from the current (real) snapshot — they gate destination fit right now, not a multi-pass trend. A VM or node absent from some prior entries (added/removed between passes) is simply averaged over however many entries it does appear in; there is no synthetic zero-fill. A missing or corrupt history file bootstraps to a single-entry window (the current snapshot only), mirroring the fail-soft load pattern used by Get-AffinityRuleSet. .OUTPUTS A PSCustomObject shaped exactly like Get-ClusterSnapshot's output (ClusterName, Timestamp, Nodes, VMs) so it is a drop-in replacement wherever a snapshot is consumed. #> [CmdletBinding()] param( [Parameter(Mandatory)] [PSCustomObject]$Snapshot, [Parameter(Mandatory)] [string]$HistoryPath, [ValidateRange(1, 10)] [int]$WindowSize = 3 ) # ── Load prior history (fail-soft: missing/corrupt file => fresh window) ────── $history = [System.Collections.Generic.List[PSCustomObject]]::new() if (Test-Path -LiteralPath $HistoryPath) { try { $data = Get-Content -LiteralPath $HistoryPath -Raw -ErrorAction Stop | ConvertFrom-Json foreach ($entry in @($data.Entries)) { $history.Add($entry) } } catch { Write-Warning "Could not load HVDRS trend history from '$HistoryPath': $_. Starting a fresh window." } } # ── Append this pass's smoothable fields only ───────────────────────────────── $currentEntry = [PSCustomObject]@{ Nodes = @($Snapshot.Nodes | ForEach-Object { [PSCustomObject]@{ NodeName = $_.NodeName CpuUtilization = [double]$_.CpuUtilization NetworkUtilization = [double]$_.NetworkUtilization } }) VMs = @($Snapshot.VMs | ForEach-Object { [PSCustomObject]@{ VMName = $_.VMName CpuUtilization = [double]$_.CpuUtilization MemoryPressure = [double]$_.MemoryPressure } }) } $history.Add($currentEntry) while ($history.Count -gt $WindowSize) { $history.RemoveAt(0) } # ── Persist the trimmed window ──────────────────────────────────────────────── $dir = Split-Path -LiteralPath $HistoryPath if ($dir -and -not (Test-Path -LiteralPath $dir)) { New-Item -Path $dir -ItemType Directory -Force | Out-Null } [PSCustomObject]@{ Version = '1.0' Entries = $history.ToArray() } | ConvertTo-Json -Depth 10 | Set-Content -LiteralPath $HistoryPath -Encoding UTF8 # ── Build the trended (averaged) snapshot ───────────────────────────────────── $trendedNodes = foreach ($node in $Snapshot.Nodes) { $samples = @($history | ForEach-Object { $_.Nodes | Where-Object { $_.NodeName -eq $node.NodeName } }) $avgCpu = if ($samples.Count -gt 0) { ($samples | Measure-Object -Property CpuUtilization -Average).Average } else { $node.CpuUtilization } $avgNet = if ($samples.Count -gt 0) { ($samples | Measure-Object -Property NetworkUtilization -Average).Average } else { $node.NetworkUtilization } [PSCustomObject]@{ NodeName = $node.NodeName CpuUtilization = [Math]::Round($avgCpu, 1) TotalMemoryMB = $node.TotalMemoryMB AvailableMemoryMB = $node.AvailableMemoryMB UsedMemoryMB = $node.UsedMemoryMB LogicalProcessorCount = $node.LogicalProcessorCount NetworkUtilization = [Math]::Round($avgNet, 1) VMs = $node.VMs } } $trendedVMs = foreach ($vm in $Snapshot.VMs) { $samples = @($history | ForEach-Object { $_.VMs | Where-Object { $_.VMName -eq $vm.VMName } }) $avgCpu = if ($samples.Count -gt 0) { ($samples | Measure-Object -Property CpuUtilization -Average).Average } else { $vm.CpuUtilization } $avgPressure = if ($samples.Count -gt 0) { ($samples | Measure-Object -Property MemoryPressure -Average).Average } else { $vm.MemoryPressure } [PSCustomObject]@{ VMName = $vm.VMName VMId = $vm.VMId CpuUtilization = [Math]::Round($avgCpu, 1) ProcessorCount = $vm.ProcessorCount MemoryAssignedMB = $vm.MemoryAssignedMB MemoryDemandMB = $vm.MemoryDemandMB DynamicMemoryEnabled = $vm.DynamicMemoryEnabled MemoryPressure = [Math]::Round($avgPressure, 1) HostNode = $vm.HostNode } } [PSCustomObject]@{ ClusterName = $Snapshot.ClusterName Timestamp = $Snapshot.Timestamp Nodes = $trendedNodes VMs = $trendedVMs } } |