diff --git a/HyperVDiag.ps1 b/HyperVDiag.ps1 new file mode 100644 index 0000000..299a607 --- /dev/null +++ b/HyperVDiag.ps1 @@ -0,0 +1,341 @@ +# 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 "" \ No newline at end of file