Windows Server Performance Monitoring: Diagnostics and Optimization
Introduction
Performance monitoring identifies bottlenecks and optimizes Windows Server resources. This guide covers Performance Monitor counters and data collectors, Resource Monitor for real-time analysis, Event Viewer for troubleshooting, Windows Admin Center dashboards, diagnostics methodology, bottleneck identification, and optimization techniques.
Performance Monitor (perfmon)
Key Performance Counters
# Essential performance counters
# CPU counters
$cpuCounters = @(
'\Processor(_Total)\% Processor Time' # Overall CPU usage (>80% sustained = bottleneck)
'\Processor(_Total)\% Privileged Time' # Kernel mode CPU (high = driver/kernel issues)
'\Processor(_Total)\% User Time' # User mode CPU (application processing)
'\System\Processor Queue Length' # CPU queue (>2 per core = bottleneck)
)
# Memory counters
$memoryCounters = @(
'\Memory\Available MBytes' # Free RAM (<100 MB = bottleneck)
'\Memory\Pages/sec' # Paging rate (>1000 sustained = bottleneck)
'\Memory\% Committed Bytes In Use' # Memory pressure
'\Paging File(_Total)\% Usage' # Page file usage
)
# Disk counters
$diskCounters = @(
'\PhysicalDisk(*)\Avg. Disk Queue Length' # Disk queue (>2 = bottleneck)
'\PhysicalDisk(*)\Avg. Disk sec/Read' # Read latency (<10ms good, >20ms poor)
'\PhysicalDisk(*)\Avg. Disk sec/Write' # Write latency
'\PhysicalDisk(*)\% Idle Time' # Disk idle percentage
'\PhysicalDisk(*)\Current Disk Queue Length' # Current queue depth
)
# Network counters
$networkCounters = @(
'\Network Interface(*)\Bytes Total/sec' # Total throughput
'\Network Interface(*)\Output Queue Length' # Send queue (>2 = congestion)
'\Network Interface(*)\Packets Received Errors' # Receive errors
'\TCPv4\Connections Established' # Active TCP connections
)
# View current values
Get-Counter -Counter $cpuCounters, $memoryCounters, $diskCounters, $networkCounters
Creating Data Collector Sets
# Create data collector set
$collectorName = "System Performance"
$outputPath = "C:\PerfLogs\$collectorName"
# Create directory
New-Item -Path $outputPath -ItemType Directory -Force
# Create data collector set using logman
logman create counter $collectorName `
-c "\Processor(_Total)\% Processor Time" `
"\Memory\Available MBytes" `
"\PhysicalDisk(*)\Avg. Disk Queue Length" `
"\Network Interface(*)\Bytes Total/sec" `
-f bin -si 00:00:05 -o "$outputPath\PerfLog.blg" -max 500
# Start data collector
logman start $collectorName
# View data collector status
logman query $collectorName
# Stop data collector
logman stop $collectorName
# Delete data collector
logman delete $collectorName
Performance Counter Alerts
# Create alert data collector set
logman create alert "High CPU Alert" `
-th "\Processor(_Total)\% Processor Time>80" `
-tn "HighCPUTask"
# Create scheduled task for alert action
$action = New-ScheduledTaskAction -Execute 'PowerShell.exe' `
-Argument '-Command "Send-MailMessage -To admin@contoso.com -From perfmon@contoso.com -Subject ''High CPU Alert'' -Body ''CPU usage exceeded 80%'' -SmtpServer smtp.contoso.com"'
Register-ScheduledTask -TaskName "HighCPUTask" -Action $action -Force
# Start alert
logman start "High CPU Alert"
Analyzing Performance Logs
# Import performance log
$log = Import-Counter -Path "C:\PerfLogs\System Performance\PerfLog.blg"
# View counter samples
$log.CounterSamples | Select-Object -First 20 | Format-Table Timestamp, Path, CookedValue
# Find peak CPU usage
$cpuSamples = $log.CounterSamples | Where-Object { $_.Path -like '*% Processor Time' }
$peakCPU = $cpuSamples | Sort-Object CookedValue -Descending | Select-Object -First 1
Write-Host "Peak CPU: $($peakCPU.CookedValue)% at $($peakCPU.Timestamp)" -ForegroundColor Yellow
# Export to CSV
$log.CounterSamples | Export-Csv -Path "C:\Reports\Performance.csv" -NoTypeInformation
Resource Monitor (resmon)
Real-Time Monitoring
# Launch Resource Monitor
Start-Process resmon.exe
# PowerShell equivalent for real-time monitoring
$monitorScript = @"
while (`$true) {
Clear-Host
Write-Host '========================================' -ForegroundColor Cyan
Write-Host 'Real-Time Resource Monitor' -ForegroundColor Cyan
Write-Host "Time: `$(Get-Date)" -ForegroundColor Cyan
Write-Host '========================================`n' -ForegroundColor Cyan
# CPU usage
Write-Host 'CPU Usage:' -ForegroundColor Yellow
Get-Counter '\Processor(_Total)\% Processor Time' |
Select-Object -ExpandProperty CounterSamples |
ForEach-Object { Write-Host " `$([math]::Round(`$_.CookedValue, 2))%" -ForegroundColor Green }
# Memory
Write-Host '`nMemory:' -ForegroundColor Yellow
`$os = Get-CimInstance Win32_OperatingSystem
`$totalMem = [math]::Round(`$os.TotalVisibleMemorySize / 1MB, 2)
`$freeMem = [math]::Round(`$os.FreePhysicalMemory / 1MB, 2)
`$usedMem = `$totalMem - `$freeMem
`$memPercent = [math]::Round((`$usedMem / `$totalMem) * 100, 2)
Write-Host " Total: `$totalMem GB" -ForegroundColor Green
Write-Host " Used: `$usedMem GB (`$memPercent%)" -ForegroundColor Green
Write-Host " Free: `$freeMem GB" -ForegroundColor Green
# Disk activity
Write-Host '`nDisk Activity:' -ForegroundColor Yellow
Get-Counter '\PhysicalDisk(_Total)\Disk Bytes/sec' |
Select-Object -ExpandProperty CounterSamples |
ForEach-Object {
`$diskMBps = [math]::Round(`$_.CookedValue / 1MB, 2)
Write-Host " `$diskMBps MB/s" -ForegroundColor Green
}
# Network
Write-Host '`nNetwork Activity:' -ForegroundColor Yellow
Get-Counter '\Network Interface(*)\Bytes Total/sec' |
Select-Object -ExpandProperty CounterSamples |
Where-Object { `$_.CookedValue -gt 0 } |
ForEach-Object {
`$netMbps = [math]::Round((`$_.CookedValue * 8) / 1MB, 2)
Write-Host " `$(`$_.InstanceName): `$netMbps Mbps" -ForegroundColor Green
}
# Top CPU processes
Write-Host '`nTop CPU Processes:' -ForegroundColor Yellow
Get-Process | Sort-Object CPU -Descending | Select-Object -First 5 |
Format-Table Name, @{N='CPU(s)';E={[math]::Round(`$_.CPU, 2)}}, @{N='Memory(MB)';E={[math]::Round(`$_.WorkingSet64 / 1MB, 2)}} -AutoSize
Start-Sleep -Seconds 2
}
"@
$monitorScript | Out-File "C:\Scripts\RealtimeMonitor.ps1"
Process-Specific Monitoring
# Monitor specific process
$processName = "w3wp"
$process = Get-Process -Name $processName
# CPU usage
$cpuBefore = $process.CPU
Start-Sleep -Seconds 1
$process = Get-Process -Name $processName
$cpuAfter = $process.CPU
$cpuUsage = $cpuAfter - $cpuBefore
Write-Host "Process: $($process.Name)" -ForegroundColor Cyan
Write-Host "CPU: $([math]::Round($cpuUsage, 2)) seconds" -ForegroundColor Yellow
Write-Host "Memory: $([math]::Round($process.WorkingSet64 / 1MB, 2)) MB" -ForegroundColor Yellow
Write-Host "Threads: $($process.Threads.Count)" -ForegroundColor Yellow
Write-Host "Handles: $($process.HandleCount)" -ForegroundColor Yellow
# Get process threads
$process.Threads | Select-Object Id, ThreadState, TotalProcessorTime | Format-Table
Event Viewer Diagnostics
Querying Event Logs
# View recent errors
Get-EventLog -LogName System -EntryType Error -Newest 50 |
Select-Object TimeGenerated, Source, EventID, Message |
Format-Table -AutoSize
# Query specific event ID
Get-EventLog -LogName System -InstanceId 1074 -Newest 20 # System shutdown events
# Search application log
Get-EventLog -LogName Application -EntryType Warning, Error -Newest 100 |
Group-Object Source |
Sort-Object Count -Descending |
Select-Object Count, Name |
Format-Table -AutoSize
# Use Get-WinEvent for advanced filtering
$filter = @{
LogName = 'System'
Level = 2 # Error
StartTime = (Get-Date).AddDays(-1)
}
Get-WinEvent -FilterHashtable $filter |
Select-Object TimeCreated, Id, LevelDisplayName, Message |
Format-Table -Wrap
Creating Custom Views
# Export custom view as XML
$viewXml = @"
<QueryList>
<Query Id="0" Path="System">
<Select Path="System">
*[System[(Level=1 or Level=2) and TimeCreated[timediff(@SystemTime) <= 86400000]]]
</Select>
</Query>
</QueryList>
"@
$viewXml | Out-File "C:\EventViews\CriticalErrors-24h.xml"
# Query using custom view
Get-WinEvent -FilterXml (Get-Content "C:\EventViews\CriticalErrors-24h.xml" -Raw)
Event Subscriptions
# Configure event forwarding (requires WinRM)
Enable-PSRemoting -Force
# On collector server, configure subscription
wecutil cs /c:"C:\EventSubscriptions\RemoteErrors.xml"
# Example subscription XML
$subscriptionXml = @"
<Subscription xmlns="http://schemas.microsoft.com/2006/03/windows/events/subscription">
<SubscriptionId>RemoteErrors</SubscriptionId>
<Description>Collect errors from remote servers</Description>
<Uri>http://schemas.microsoft.com/wbem/wsman/1/windows/EventLog</Uri>
<ConfigurationMode>Custom</ConfigurationMode>
<Delivery Mode="Push">
<Batching>
<MaxLatencyTime>300000</MaxLatencyTime>
</Batching>
<PushSettings>
<Heartbeat Interval="3600000"/>
</PushSettings>
</Delivery>
<Query>
<![CDATA[
<QueryList>
<Query Id="0">
<Select Path="System">*[System[(Level=1 or Level=2)]]</Select>
</Query>
</QueryList>
]]>
</Query>
<ReadExistingEvents>true</ReadExistingEvents>
<TransportName>http</TransportName>
<ContentFormat>RenderedText</ContentFormat>
<Locale Language="en-US"/>
</Subscription>
"@
$subscriptionXml | Out-File "C:\EventSubscriptions\RemoteErrors.xml"
Troubleshooting Methodology
Systematic Diagnostics
# Performance diagnostics script
$diagScript = @"
Write-Host '========================================' -ForegroundColor Cyan
Write-Host 'Windows Server Performance Diagnostics' -ForegroundColor Cyan
Write-Host '========================================`n' -ForegroundColor Cyan
# 1. Identify symptoms
Write-Host '1. System Overview:' -ForegroundColor Yellow
`$os = Get-CimInstance Win32_OperatingSystem
Write-Host " OS: `$(`$os.Caption)" -ForegroundColor Green
Write-Host " Uptime: `$(([datetime]::Now - `$os.LastBootUpTime).Days) days" -ForegroundColor Green
# 2. Check CPU
Write-Host '`n2. CPU Analysis:' -ForegroundColor Yellow
`$cpu = Get-Counter '\Processor(_Total)\% Processor Time' | Select-Object -ExpandProperty CounterSamples
Write-Host " CPU Usage: `$([math]::Round(`$cpu.CookedValue, 2))%" -ForegroundColor $(if (`$cpu.CookedValue -gt 80) {'Red'} else {'Green'})
`$topCpuProcesses = Get-Process | Sort-Object CPU -Descending | Select-Object -First 5
Write-Host " Top CPU Processes:" -ForegroundColor Green
`$topCpuProcesses | Format-Table Name, @{N='CPU(s)';E={[math]::Round(`$_.CPU, 2)}} -AutoSize
# 3. Check Memory
Write-Host '3. Memory Analysis:' -ForegroundColor Yellow
`$freeMem = [math]::Round(`$os.FreePhysicalMemory / 1MB, 2)
Write-Host " Available Memory: `$freeMem GB" -ForegroundColor $(if (`$freeMem -lt 1) {'Red'} elseif (`$freeMem -lt 2) {'Yellow'} else {'Green'})
`$topMemProcesses = Get-Process | Sort-Object WorkingSet64 -Descending | Select-Object -First 5
Write-Host " Top Memory Processes:" -ForegroundColor Green
`$topMemProcesses | Format-Table Name, @{N='Memory(MB)';E={[math]::Round(`$_.WorkingSet64 / 1MB, 2)}} -AutoSize
# 4. Check Disk
Write-Host '4. Disk Analysis:' -ForegroundColor Yellow
Get-Volume | Where-Object { `$_.DriveLetter } | ForEach-Object {
`$freePercent = [math]::Round((`$_.SizeRemaining / `$_.Size) * 100, 2)
`$color = if (`$freePercent -lt 10) {'Red'} elseif (`$freePercent -lt 20) {'Yellow'} else {'Green'}
Write-Host " `$(`$_.DriveLetter): `$freePercent% free" -ForegroundColor `$color
}
`$diskQueue = Get-Counter '\PhysicalDisk(_Total)\Avg. Disk Queue Length' | Select-Object -ExpandProperty CounterSamples
Write-Host " Disk Queue: `$([math]::Round(`$diskQueue.CookedValue, 2))" -ForegroundColor $(if (`$diskQueue.CookedValue -gt 2) {'Red'} else {'Green'})
# 5. Check Network
Write-Host '`n5. Network Analysis:' -ForegroundColor Yellow
Get-NetAdapter | Where-Object { `$_.Status -eq 'Up' } | ForEach-Object {
Write-Host " `$(`$_.Name): `$(`$_.LinkSpeed)" -ForegroundColor Green
}
# 6. Recent errors
Write-Host '`n6. Recent Errors (Last 24h):' -ForegroundColor Yellow
`$errors = Get-EventLog -LogName System -EntryType Error -After (Get-Date).AddDays(-1) -ErrorAction SilentlyContinue
Write-Host " System Errors: `$(`$errors.Count)" -ForegroundColor $(if (`$errors.Count -gt 10) {'Red'} else {'Green'})
if (`$errors.Count -gt 0) {
`$errorGroups = `$errors | Group-Object Source | Sort-Object Count -Descending | Select-Object -First 5
Write-Host " Top Error Sources:" -ForegroundColor Green
`$errorGroups | Format-Table Count, Name -AutoSize
}
Write-Host '`n========================================' -ForegroundColor Cyan
Write-Host 'Diagnostics Complete' -ForegroundColor Cyan
Write-Host '========================================' -ForegroundColor Cyan
"@
$diagScript | Out-File "C:\Scripts\PerformanceDiag.ps1"
Bottleneck Identification
# Bottleneck detection script
$bottleneckScript = @"
function Test-CPUBottleneck {
`$cpu = (Get-Counter '\Processor(_Total)\% Processor Time').CounterSamples.CookedValue
`$queueLength = (Get-Counter '\System\Processor Queue Length').CounterSamples.CookedValue
if (`$cpu -gt 80 -or `$queueLength -gt 10) {
[PSCustomObject]@{
Type = 'CPU'
Severity = if (`$cpu -gt 90) {'Critical'} else {'Warning'}
Details = "CPU: `$([math]::Round(`$cpu, 2))%, Queue: `$queueLength"
Recommendation = 'Identify high CPU processes. Consider adding CPU cores or optimizing applications.'
}
}
}
function Test-MemoryBottleneck {
`$os = Get-CimInstance Win32_OperatingSystem
`$freeGB = `$os.FreePhysicalMemory / 1MB
`$pagesPerSec = (Get-Counter '\Memory\Pages/sec').CounterSamples.CookedValue
if (`$freeGB -lt 1 -or `$pagesPerSec -gt 1000) {
[PSCustomObject]@{
Type = 'Memory'
Severity = if (`$freeGB -lt 0.5) {'Critical'} else {'Warning'}
Details = "Free: `$([math]::Round(`$freeGB, 2)) GB, Paging: `$([math]::Round(`$pagesPerSec, 0))/sec"
Recommendation = 'Close unnecessary applications. Add more RAM or optimize memory usage.'
}
}
}
function Test-DiskBottleneck {
`$diskQueue = (Get-Counter '\PhysicalDisk(_Total)\Avg. Disk Queue Length').CounterSamples.CookedValue
`$diskLatency = (Get-Counter '\PhysicalDisk(_Total)\Avg. Disk sec/Read').CounterSamples.CookedValue * 1000
if (`$diskQueue -gt 2 -or `$diskLatency -gt 20) {
[PSCustomObject]@{
Type = 'Disk'
Severity = if (`$diskQueue -gt 5 -or `$diskLatency -gt 50) {'Critical'} else {'Warning'}
Details = "Queue: `$([math]::Round(`$diskQueue, 2)), Latency: `$([math]::Round(`$diskLatency, 2)) ms"
Recommendation = 'Check disk health. Upgrade to SSD or add more disks. Optimize I/O patterns.'
}
}
}
function Test-NetworkBottleneck {
`$adapters = Get-NetAdapter | Where-Object { `$_.Status -eq 'Up' }
foreach (`$adapter in `$adapters) {
`$counter = Get-Counter "\Network Interface(`$(`$adapter.InterfaceDescription))\Output Queue Length" -ErrorAction SilentlyContinue
if (`$counter -and `$counter.CounterSamples.CookedValue -gt 2) {
[PSCustomObject]@{
Type = 'Network'
Severity = 'Warning'
Details = "`$(`$adapter.Name): Output queue `$(`$counter.CounterSamples.CookedValue)"
Recommendation = 'Check network utilization. Upgrade network adapter or optimize network traffic.'
}
}
}
}
Write-Host 'Scanning for performance bottlenecks...' -ForegroundColor Cyan
`$bottlenecks = @()
`$bottlenecks += Test-CPUBottleneck
`$bottlenecks += Test-MemoryBottleneck
`$bottlenecks += Test-DiskBottleneck
`$bottlenecks += Test-NetworkBottleneck
if (`$bottlenecks) {
Write-Host '`nBottlenecks detected:' -ForegroundColor Red
`$bottlenecks | Format-Table Type, Severity, Details, Recommendation -Wrap
} else {
Write-Host '`nNo bottlenecks detected. System performance is healthy.' -ForegroundColor Green
}
"@
$bottleneckScript | Out-File "C:\Scripts\DetectBottlenecks.ps1"
Optimization Techniques
Service Optimization
# Disable unnecessary services
$servicesToDisable = @(
'XblAuthManager' # Xbox Live Auth Manager
'XblGameSave' # Xbox Live Game Save
'XboxGipSvc' # Xbox Accessory Management
'XboxNetApiSvc' # Xbox Live Networking
)
foreach ($service in $servicesToDisable) {
$svc = Get-Service -Name $service -ErrorAction SilentlyContinue
if ($svc) {
Stop-Service -Name $service -Force
Set-Service -Name $service -StartupType Disabled
Write-Host "Disabled: $service" -ForegroundColor Green
}
}
Visual Effects Optimization
# Disable visual effects for better performance
$regPath = "HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\VisualEffects"
Set-ItemProperty -Path $regPath -Name "VisualFXSetting" -Value 2 # Best performance
# Disable animations
Set-ItemProperty -Path "HKCU:\Control Panel\Desktop\WindowMetrics" -Name "MinAnimate" -Value "0"
Startup Optimization
# List startup programs
Get-CimInstance Win32_StartupCommand |
Select-Object Name, Command, Location, User |
Format-Table -AutoSize
# Disable startup program (use Task Manager or modify registry)
# Remove-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run" -Name "ProgramName"
Key Takeaways
- Performance Monitor tracks CPU, memory, disk, and network metrics
- Data collector sets gather performance data over time
- Resource Monitor provides real-time resource analysis
- Event Viewer diagnoses system and application errors
- Systematic methodology identifies performance bottlenecks
- CPU, memory, disk, and network bottlenecks require different solutions
- Service and startup optimization improves system performance
- Regular monitoring prevents performance degradation
Next Steps
- Set up performance monitoring with data collectors
- Create custom Event Viewer filters for critical events
- Implement automated performance diagnostics
- Establish performance baselines
- Schedule regular bottleneck scans
- Optimize services and startup programs
- Document performance trends
Additional Resources
Monitor. Diagnose. Optimize. Improve.