Windows Server Performance Monitoring: Diagnostics and Optimization

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) &lt;= 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.