# Hyper-V Performance Diagnostic Script # Diagnoses issues after checkpoint/merge operations param( [switch]$Detailed, [switch]$ExportReport ) $ReportPath = "C:\Temp\HyperV-Diagnostic-$(Get-Date -Format 'yyyyMMdd_HHmmss').txt" function Write-Output-And-Log { param([string]$Message, [string]$Color = "White") Write-Host $Message -ForegroundColor $Color if ($ExportReport) { Add-Content -Path $ReportPath -Value $Message } } function Write-Section { param([string]$Title) Write-Output-And-Log "" Write-Output-And-Log "============================================" "Cyan" Write-Output-And-Log $Title "Cyan" Write-Output-And-Log "============================================" "Cyan" } try { if ($ExportReport) { New-Item -ItemType Directory -Path (Split-Path $ReportPath -Parent) -Force -ErrorAction SilentlyContinue | Out-Null Write-Output-And-Log "Diagnostic Report will be saved to: $ReportPath" "Yellow" } Write-Section "HYPER-V PERFORMANCE DIAGNOSTIC - $(Get-Date)" # 1. Check for active merge operations Write-Section "1. CHECKING FOR ACTIVE MERGE OPERATIONS" $VMs = Get-VM $MergingVMs = @() foreach ($VM in $VMs) { $Status = $VM.Status if ($Status -like "*Merging*" -or $Status -like "*Deleting*") { $MergingVMs += $VM Write-Output-And-Log "⚠️ $($VM.Name): $Status" "Yellow" } else { Write-Output-And-Log "✓ $($VM.Name): $Status" "Green" } } if ($MergingVMs.Count -gt 0) { Write-Output-And-Log "" Write-Output-And-Log "⚠️ WARNING: $($MergingVMs.Count) VM(s) still have merge operations in progress!" "Red" Write-Output-And-Log "This is likely causing the slowness. Wait for merges to complete." "Red" } else { Write-Output-And-Log "" Write-Output-And-Log "✓ No active merge operations detected" "Green" } # 2. Check disk space Write-Section "2. DISK SPACE CHECK" $Drives = Get-PSDrive -PSProvider FileSystem | Where-Object { $_.Used -gt 0 } foreach ($Drive in $Drives) { $UsedGB = [math]::Round($Drive.Used / 1GB, 2) $FreeGB = [math]::Round($Drive.Free / 1GB, 2) $TotalGB = $UsedGB + $FreeGB $PercentFree = [math]::Round(($FreeGB / $TotalGB) * 100, 1) $Color = "Green" $Warning = "" if ($PercentFree -lt 10) { $Color = "Red" $Warning = " ⚠️ CRITICAL - Less than 10% free!" } elseif ($PercentFree -lt 20) { $Color = "Yellow" $Warning = " ⚠️ WARNING - Less than 20% free" } Write-Output-And-Log "$($Drive.Name) - Free: $FreeGB GB / Total: $TotalGB GB ($PercentFree% free)$Warning" $Color } # 3. Check checkpoint status for all VMs Write-Section "3. CHECKPOINT STATUS FOR ALL VMs" foreach ($VM in $VMs) { $Checkpoints = Get-VMCheckpoint -VMName $VM.Name $CheckpointCount = $Checkpoints.Count if ($CheckpointCount -eq 0) { Write-Output-And-Log "$($VM.Name): No checkpoints" "Green" } else { Write-Output-And-Log "$($VM.Name): $CheckpointCount checkpoint(s)" "Yellow" foreach ($Checkpoint in $Checkpoints | Sort-Object CreationTime) { $Age = [math]::Round((New-TimeSpan -Start $Checkpoint.CreationTime -End (Get-Date)).TotalDays, 1) Write-Output-And-Log " - $($Checkpoint.Name) (Age: $Age days, Created: $($Checkpoint.CreationTime))" } } } # 4. Check for AVHDX files and their sizes Write-Section "4. DIFFERENCING DISK (AVHDX) FILES" $TotalAVHDXSize = 0 $AVHDXFiles = @() foreach ($VM in $VMs) { $VMHardDisks = Get-VMHardDiskDrive -VMName $VM.Name foreach ($Disk in $VMHardDisks) { if ($Disk.Path -match "\.avhdx$") { if (Test-Path $Disk.Path) { $FileInfo = Get-Item $Disk.Path $SizeGB = [math]::Round($FileInfo.Length / 1GB, 2) $TotalAVHDXSize += $FileInfo.Length $AVHDXFiles += [PSCustomObject]@{ VM = $VM.Name Path = $Disk.Path SizeGB = $SizeGB LastModified = $FileInfo.LastWriteTime } } } } } if ($AVHDXFiles.Count -eq 0) { Write-Output-And-Log "✓ No AVHDX files found (no active checkpoints)" "Green" } else { $TotalAVHDXGB = [math]::Round($TotalAVHDXSize / 1GB, 2) Write-Output-And-Log "⚠️ Found $($AVHDXFiles.Count) AVHDX files (Total: $TotalAVHDXGB GB)" "Yellow" Write-Output-And-Log "" foreach ($File in $AVHDXFiles | Sort-Object SizeGB -Descending) { Write-Output-And-Log "$($File.VM): $($File.SizeGB) GB - $(Split-Path $File.Path -Leaf)" Write-Output-And-Log " Last Modified: $($File.LastModified)" } } # 5. Check disk I/O performance Write-Section "5. CURRENT DISK I/O ACTIVITY" Write-Output-And-Log "Sampling disk activity for 5 seconds..." $DiskCounters = Get-Counter '\PhysicalDisk(*)\Disk Writes/sec', '\PhysicalDisk(*)\Disk Reads/sec' -SampleInterval 1 -MaxSamples 5 $Writes = $DiskCounters.CounterSamples | Where-Object { $_.Path -like "*writes*" } | Group-Object { $_.InstanceName } | ForEach-Object { [PSCustomObject]@{ Disk = $_.Name; AvgWrites = [math]::Round(($_.Group.CookedValue | Measure-Object -Average).Average, 1) }} $Reads = $DiskCounters.CounterSamples | Where-Object { $_.Path -like "*reads*" } | Group-Object { $_.InstanceName } | ForEach-Object { [PSCustomObject]@{ Disk = $_.Name; AvgReads = [math]::Round(($_.Group.CookedValue | Measure-Object -Average).Average, 1) }} Write-Output-And-Log "" Write-Output-And-Log "Average Disk Activity (5 second sample):" foreach ($Disk in $Writes) { if ($Disk.Disk -ne "_total") { $ReadInfo = $Reads | Where-Object { $_.Disk -eq $Disk.Disk } $TotalIO = $Disk.AvgWrites + $ReadInfo.AvgReads $Color = "Green" if ($TotalIO -gt 100) { $Color = "Yellow" } if ($TotalIO -gt 500) { $Color = "Red" } Write-Output-And-Log "$($Disk.Disk): Reads: $($ReadInfo.AvgReads)/sec, Writes: $($Disk.AvgWrites)/sec" $Color } } # 6. Check CPU and Memory usage Write-Section "6. SYSTEM RESOURCE USAGE" $CPU = Get-Counter '\Processor(_Total)\% Processor Time' -SampleInterval 1 -MaxSamples 5 $AvgCPU = [math]::Round(($CPU.CounterSamples.CookedValue | Measure-Object -Average).Average, 1) $CPUColor = "Green" if ($AvgCPU -gt 70) { $CPUColor = "Yellow" } if ($AvgCPU -gt 90) { $CPUColor = "Red" } Write-Output-And-Log "Average CPU Usage: $AvgCPU%" $CPUColor $OS = Get-CimInstance -ClassName Win32_OperatingSystem $TotalRAM = [math]::Round($OS.TotalVisibleMemorySize / 1MB, 2) $FreeRAM = [math]::Round($OS.FreePhysicalMemory / 1MB, 2) $UsedRAM = $TotalRAM - $FreeRAM $PercentUsed = [math]::Round(($UsedRAM / $TotalRAM) * 100, 1) $RAMColor = "Green" if ($PercentUsed -gt 80) { $RAMColor = "Yellow" } if ($PercentUsed -gt 95) { $RAMColor = "Red" } Write-Output-And-Log "Memory Usage: $UsedRAM GB / $TotalRAM GB ($PercentUsed% used)" $RAMColor # 7. Check VM Storage Locations Write-Section "7. VM STORAGE LOCATIONS" foreach ($VM in $VMs) { Write-Output-And-Log "" Write-Output-And-Log "$($VM.Name):" Write-Output-And-Log " Config: $($VM.ConfigurationLocation)" Write-Output-And-Log " Snapshots: $($VM.SnapshotFileLocation)" $VMHardDisks = Get-VMHardDiskDrive -VMName $VM.Name foreach ($Disk in $VMHardDisks) { Write-Output-And-Log " Disk: $($Disk.Path)" } } # 8. Check recent Hyper-V errors Write-Section "8. RECENT HYPER-V ERRORS (Last 24 hours)" $Yesterday = (Get-Date).AddDays(-1) $HyperVErrors = Get-WinEvent -FilterHashtable @{ LogName = 'Microsoft-Windows-Hyper-V-*' Level = 2,3 # Error and Warning StartTime = $Yesterday } -ErrorAction SilentlyContinue | Select-Object -First 10 if ($HyperVErrors) { Write-Output-And-Log "⚠️ Found $($HyperVErrors.Count) recent error/warning entries (showing first 10):" "Yellow" foreach ($Error in $HyperVErrors) { Write-Output-And-Log "" Write-Output-And-Log "[$($Error.TimeCreated)] $($Error.LevelDisplayName): $($Error.Message)" "Yellow" } } else { Write-Output-And-Log "✓ No recent errors found in Hyper-V event logs" "Green" } # 9. Detailed checkpoint analysis if requested if ($Detailed) { Write-Section "9. DETAILED CHECKPOINT ANALYSIS" foreach ($VM in $VMs) { $Checkpoints = Get-VMCheckpoint -VMName $VM.Name if ($Checkpoints.Count -gt 0) { Write-Output-And-Log "" Write-Output-And-Log "$($VM.Name) - Detailed Checkpoint Info:" foreach ($Checkpoint in $Checkpoints) { Write-Output-And-Log " Checkpoint: $($Checkpoint.Name)" Write-Output-And-Log " ID: $($Checkpoint.Id)" Write-Output-And-Log " Type: $($Checkpoint.CheckpointType)" Write-Output-And-Log " Created: $($Checkpoint.CreationTime)" Write-Output-And-Log " Parent: $($Checkpoint.ParentCheckpointName)" # Try to get AVHDX sizes for this checkpoint $CheckpointDisks = Get-VMHardDiskDrive -VMCheckpoint $Checkpoint -ErrorAction SilentlyContinue if ($CheckpointDisks) { foreach ($Disk in $CheckpointDisks) { if (Test-Path $Disk.Path) { $Size = [math]::Round((Get-Item $Disk.Path).Length / 1GB, 2) Write-Output-And-Log " Disk: $Size GB - $(Split-Path $Disk.Path -Leaf)" } } } } } } } # Summary and Recommendations Write-Section "SUMMARY AND RECOMMENDATIONS" $Issues = @() if ($MergingVMs.Count -gt 0) { $Issues += "⚠️ CRITICAL: Merge operations still in progress on $($MergingVMs.Count) VM(s)" $Issues += " Action: Wait for merges to complete or restart Hyper-V service (risky)" } if ($AVHDXFiles.Count -gt 0) { $Issues += "⚠️ WARNING: $($AVHDXFiles.Count) checkpoint differencing disks (AVHDX files) exist" $Issues += " Action: Remove old checkpoints to merge these files" } $LowDiskDrives = $Drives | Where-Object { ($_.Free / ($_.Used + $_.Free)) -lt 0.20 } if ($LowDiskDrives) { $Issues += "⚠️ WARNING: Low disk space on $($LowDiskDrives.Count) drive(s)" $Issues += " Action: Free up disk space or move VMs to larger drives" } if ($AvgCPU -gt 80) { $Issues += "⚠️ WARNING: High CPU usage ($AvgCPU%)" $Issues += " Action: Check for runaway processes or reduce VM load" } if ($PercentUsed -gt 90) { $Issues += "⚠️ WARNING: High memory usage ($PercentUsed%)" $Issues += " Action: Reduce VM memory allocations or add more RAM" } if ($Issues.Count -eq 0) { Write-Output-And-Log "✓ No major issues detected!" "Green" Write-Output-And-Log "" Write-Output-And-Log "Your slowness might be due to:" Write-Output-And-Log "1. Background merge operations that completed but haven't settled" Write-Output-And-Log "2. Disk fragmentation after large merge operations" Write-Output-And-Log "3. Windows Search or other background services indexing new data" Write-Output-And-Log "" Write-Output-And-Log "Recommendations:" Write-Output-And-Log "- Restart the Hyper-V Host to clear any stuck states" Write-Output-And-Log "- Defragment drives containing VM files" Write-Output-And-Log "- Check Windows Task Manager for background processes" } else { foreach ($Issue in $Issues) { Write-Output-And-Log $Issue "Red" } } Write-Output-And-Log "" Write-Output-And-Log "============================================" "Cyan" Write-Output-And-Log "DIAGNOSTIC COMPLETE - $(Get-Date)" "Cyan" Write-Output-And-Log "============================================" "Cyan" if ($ExportReport) { Write-Output-And-Log "" Write-Output-And-Log "Report saved to: $ReportPath" "Green" } } catch { Write-Output-And-Log "Error running diagnostic: $_" "Red" } # Quick commands to try Write-Output-And-Log "" Write-Output-And-Log "============================================" "Yellow" Write-Output-And-Log "QUICK DIAGNOSTIC COMMANDS" "Yellow" Write-Output-And-Log "============================================" "Yellow" Write-Output-And-Log "" Write-Output-And-Log "Check for stuck merge operations:" Write-Output-And-Log " Get-VM | Where-Object {`$_.Status -like '*Merging*'}" "Cyan" Write-Output-And-Log "" Write-Output-And-Log "Check disk queue length (high = problem):" Write-Output-And-Log " Get-Counter '\PhysicalDisk(*)\Avg. Disk Queue Length'" "Cyan" Write-Output-And-Log "" Write-Output-And-Log "View all checkpoints:" Write-Output-And-Log " Get-VM | ForEach-Object { Get-VMCheckpoint -VMName `$_.Name }" "Cyan" Write-Output-And-Log "" Write-Output-And-Log "Restart Hyper-V Management Service (last resort):" Write-Output-And-Log " Restart-Service vmms -Force" "Cyan" Write-Output-And-Log ""