308 lines
17 KiB
PowerShell
308 lines
17 KiB
PowerShell
<#
|
|
laptop-check.ps1 v2 - READ-ONLY laptop performance / battery / throttling check
|
|
Abyres Support. Makes NO changes. Takes about 1 minute (includes a 30 s CPU load test).
|
|
|
|
Run in PowerShell as Administrator:
|
|
powershell -ExecutionPolicy Bypass -File "<path>\laptop-check.ps1"
|
|
powershell -ExecutionPolicy Bypass -File "<path>\laptop-check.ps1" -NoLoad # skip load test
|
|
powershell -ExecutionPolicy Bypass -File "<path>\laptop-check.ps1" -LoadSec 60 # longer load test
|
|
|
|
Output on Desktop:
|
|
laptop-check-<PC>-<time>.txt (results + [FLAG] lines + summary)
|
|
battery-report-<PC>-<time>.html (Windows battery report)
|
|
|
|
Run once PLUGGED IN and once ON BATTERY. Close other apps first.
|
|
#>
|
|
param([switch]$NoLoad, [int]$LoadSec = 30)
|
|
|
|
$ErrorActionPreference = 'SilentlyContinue'
|
|
$TS = Get-Date -Format 'yyyyMMdd-HHmmss'
|
|
$PC = $env:COMPUTERNAME
|
|
$Desk = [Environment]::GetFolderPath('Desktop')
|
|
$Out = Join-Path $Desk "laptop-check-$PC-$TS.txt"
|
|
$BatHtml = Join-Path $Desk "battery-report-$PC-$TS.html"
|
|
$Flags = New-Object System.Collections.Generic.List[string]
|
|
if ($LoadSec -lt 15) { $LoadSec = 15 }
|
|
|
|
$modes = @{
|
|
'961cc777-2547-4f9d-8174-7d86181b8a7a' = 'Best power efficiency'
|
|
'3af9b8d9-7c97-431d-ad78-34a8bfea439f' = 'Better battery'
|
|
'00000000-0000-0000-0000-000000000000' = 'Balanced'
|
|
'ded574b5-45a0-4f42-8737-46345c09c238' = 'Best performance'
|
|
}
|
|
|
|
function Say ($t) { "`n===== $t =====" }
|
|
function Ok ($t) { " [OK] $t" }
|
|
function Info($t) { " [INFO] $t" }
|
|
function Flag($t) { $Flags.Add($t); " [FLAG] $t" }
|
|
function Cnt ($x) { if ($x) { @($x).Count } else { 0 } }
|
|
function ModeName($g) { $s = "$g".ToLower(); if ($modes.ContainsKey($s)) { $modes[$s] } else { "custom ($s)" } }
|
|
function AvgC($samples, $pat) {
|
|
$v = ($samples | Where-Object { $_.Path -like $pat } | Measure-Object CookedValue -Average).Average
|
|
if ($v -ne $null) { [math]::Round($v, 1) }
|
|
}
|
|
function Get-Temps {
|
|
Get-CimInstance -Namespace root\wmi -ClassName MSAcpi_ThermalZoneTemperature | ForEach-Object {
|
|
[pscustomobject]@{ Zone = ($_.InstanceName -split '\\')[-1]; C = [math]::Round($_.CurrentTemperature / 10 - 273.15) }
|
|
}
|
|
}
|
|
|
|
Add-Type -Namespace Abyres -Name Pwr -MemberDefinition @'
|
|
[DllImport("powrprof.dll")] public static extern uint PowerGetEffectiveOverlayScheme(out Guid g);
|
|
[DllImport("powrprof.dll")] public static extern uint PowerGetUserConfiguredACPowerMode(out Guid g);
|
|
[DllImport("powrprof.dll")] public static extern uint PowerGetUserConfiguredDCPowerMode(out Guid g);
|
|
'@
|
|
|
|
& {
|
|
# ---------------------------------------------------------------------------
|
|
Say "LAPTOP CHECK v2 - $PC - $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - user $env:USERNAME"
|
|
$isAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
|
|
if (-not $isAdmin) { Flag 'Not run as Administrator - battery/temperature/event checks may be incomplete' }
|
|
|
|
$cs = Get-CimInstance Win32_ComputerSystem
|
|
$os = Get-CimInstance Win32_OperatingSystem
|
|
$bios = Get-CimInstance Win32_BIOS
|
|
$cpu = Get-CimInstance Win32_Processor | Select-Object -First 1
|
|
$n = [int]$cs.NumberOfLogicalProcessors
|
|
if ($n -lt 1) { $n = 1 }
|
|
|
|
Say 'SYSTEM'
|
|
Info "Model : $($cs.Manufacturer) $($cs.Model) (S/N $($bios.SerialNumber))"
|
|
Info "OS : $($os.Caption) build $($os.BuildNumber)"
|
|
Info "BIOS : $($bios.SMBIOSBIOSVersion) ($($bios.ReleaseDate))"
|
|
Info "CPU : $($cpu.Name) - $($cpu.NumberOfCores) cores / $n threads, base $($cpu.MaxClockSpeed) MHz"
|
|
$up = (Get-Date) - $os.LastBootUpTime
|
|
if ($up.TotalDays -gt 7) { Flag "Uptime $([int]$up.TotalDays) days - do a full Restart (Shut down keeps uptime with Fast Startup)" }
|
|
else { Ok "Uptime $([math]::Round($up.TotalHours,1)) h" }
|
|
|
|
# ---------------------------------------------------------------------------
|
|
Say 'POWER SOURCE AND MODE'
|
|
$bat = Get-CimInstance Win32_Battery | Select-Object -First 1
|
|
$bs = Get-CimInstance -Namespace root\wmi -ClassName BatteryStatus | Select-Object -First 1
|
|
if ($bs) { $onAC = [bool]$bs.PowerOnline } elseif ($bat) { $onAC = ($bat.BatteryStatus -ne 1) } else { $onAC = $true }
|
|
if ($onAC) { $src = 'PLUGGED IN' } else { $src = 'ON BATTERY' }
|
|
Info "Power source : $src"
|
|
if ($bat) {
|
|
Info "Charge : $($bat.EstimatedChargeRemaining)%"
|
|
if ($bs) {
|
|
Info "Charging : $($bs.Charging) (charge rate $($bs.ChargeRate) mW, discharge rate $($bs.DischargeRate) mW)"
|
|
if ($onAC -and -not $bs.Charging -and $bat.EstimatedChargeRemaining -lt 95) {
|
|
Flag "Plugged in but NOT charging at $($bat.EstimatedChargeRemaining)% - check adapter (wattage/genuine), DC port, battery, or an OEM charge-limit setting"
|
|
}
|
|
}
|
|
} else { Info 'No battery detected' }
|
|
Info ("Power plan : " + (powercfg /getactivescheme | Out-String).Trim())
|
|
|
|
# Power mode: Windows API first (matches Settings), registry as fallback
|
|
$g = [Guid]::Empty; $apiAC = $null; $apiDC = $null; $apiEff = $null
|
|
try { if ([Abyres.Pwr]::PowerGetUserConfiguredACPowerMode([ref]$g) -eq 0) { $apiAC = $g } } catch {}
|
|
$g = [Guid]::Empty
|
|
try { if ([Abyres.Pwr]::PowerGetUserConfiguredDCPowerMode([ref]$g) -eq 0) { $apiDC = $g } } catch {}
|
|
$g = [Guid]::Empty
|
|
try { if ([Abyres.Pwr]::PowerGetEffectiveOverlayScheme([ref]$g) -eq 0) { $apiEff = $g } } catch {}
|
|
$rk = Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\Power\User\PowerSchemes'
|
|
foreach ($k in 'Ac','Dc') {
|
|
if ($k -eq 'Ac') { $api = $apiAC; $lbl = 'plugged in' } else { $api = $apiDC; $lbl = 'on battery' }
|
|
if ($api -ne $null) { $name = ModeName $api; $how = 'Windows API' }
|
|
else {
|
|
$rg = $rk."ActiveOverlay${k}PowerScheme"; if (-not $rg) { $rg = '00000000-0000-0000-0000-000000000000' }
|
|
$name = ModeName $rg; $how = 'registry'
|
|
}
|
|
if ($k -eq 'Ac' -and $name -eq 'Best power efficiency') { Flag "Power mode $lbl = $name (slows CPU) - set to Balanced/Best performance [$how]" }
|
|
else { Info "Power mode $lbl = $name [$how]" }
|
|
}
|
|
if ($apiEff -ne $null) { Info "Power mode in effect now = $(ModeName $apiEff)" }
|
|
|
|
$q = powercfg /query SCHEME_CURRENT SUB_PROCESSOR PROCTHROTTLEMAX
|
|
foreach ($k in 'AC','DC') {
|
|
$m = $q | Select-String "Current $k Power Setting Index:\s*0x([0-9a-fA-F]+)"
|
|
if ($m) {
|
|
$v = [Convert]::ToInt32($m.Matches[0].Groups[1].Value, 16)
|
|
if ($v -lt 100) { Flag "Max processor state ($k) = $v% - CPU capped by power plan" } else { Ok "Max processor state ($k) = $v%" }
|
|
}
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
Say 'BATTERY HEALTH'
|
|
if ($bat) {
|
|
powercfg /batteryreport /output "$BatHtml" | Out-Null
|
|
$design = (Get-CimInstance -Namespace root\wmi -ClassName BatteryStaticData | Select-Object -First 1).DesignedCapacity
|
|
$full = (Get-CimInstance -Namespace root\wmi -ClassName BatteryFullChargedCapacity | Select-Object -First 1).FullChargedCapacity
|
|
$cycles = (Get-CimInstance -Namespace root\wmi -ClassName BatteryCycleCount | Select-Object -First 1).CycleCount
|
|
$how = 'WMI'
|
|
if (-not ($design -and $full) -and (Test-Path $BatHtml)) {
|
|
$html = Get-Content $BatHtml -Raw
|
|
$num = {
|
|
param($label)
|
|
$m = [regex]::Match($html, "$label\s*</span>\s*</td>\s*<td>\s*([\d\.,\s]+?)\s*mWh", 'IgnoreCase')
|
|
if ($m.Success) { [int64]($m.Groups[1].Value -replace '[^\d]', '') }
|
|
}
|
|
$design = & $num 'DESIGN CAPACITY'
|
|
$full = & $num 'FULL CHARGE CAPACITY'
|
|
$mc = [regex]::Match($html, 'CYCLE COUNT\s*</span>\s*</td>\s*<td>\s*(\d+)', 'IgnoreCase')
|
|
if ($mc.Success) { $cycles = [int]$mc.Groups[1].Value }
|
|
$how = 'battery report'
|
|
}
|
|
if ($design -and $full) {
|
|
$h = [math]::Round($full / $design * 100)
|
|
$t = "Full charge $full mWh of $design mWh design = $h% [$how]"
|
|
if ($h -lt 60) { Flag "Battery badly worn: $t - replacement recommended" }
|
|
elseif ($h -lt 80) { Flag "Battery degraded: $t" }
|
|
else { Ok $t }
|
|
} else { Flag 'Battery capacity could not be read - open the HTML battery report manually' }
|
|
if ($cycles) { Info "Cycle count : $cycles" }
|
|
if (Test-Path $BatHtml) { Info "Battery report: $BatHtml" }
|
|
} else { Info 'No battery' }
|
|
|
|
# ---------------------------------------------------------------------------
|
|
Say 'CPU AT CURRENT USE (sampling ~10 s)'
|
|
$p1 = @{}
|
|
Get-Process | ForEach-Object { $p1[$_.Id] = $_.TotalProcessorTime.TotalSeconds }
|
|
$sw = [Diagnostics.Stopwatch]::StartNew()
|
|
$ctr = Get-Counter -Counter '\Processor(_Total)\% Processor Time','\Processor(_Total)\% Interrupt Time','\Processor(_Total)\% DPC Time','\Processor Information(_Total)\% Processor Performance' -SampleInterval 1 -MaxSamples 10
|
|
if ($sw.Elapsed.TotalSeconds -lt 5) { Start-Sleep -Seconds 10 }
|
|
$sec = $sw.Elapsed.TotalSeconds
|
|
$p2 = Get-Process
|
|
$cs1 = $ctr.CounterSamples
|
|
|
|
$load = AvgC $cs1 '*% processor time'
|
|
$perf = AvgC $cs1 '*% processor performance'
|
|
$irq = AvgC $cs1 '*% interrupt time'
|
|
$dpc = AvgC $cs1 '*% dpc time'
|
|
if ($load -ne $null) {
|
|
if ($load -ge 80) { Flag "CPU load averaged $load% over $([int]$sec) s" } else { Ok "CPU load averaged $load%" }
|
|
}
|
|
if ($irq -ne $null -and $dpc -ne $null) {
|
|
$t = "Interrupt $irq% + DPC $dpc% of CPU"
|
|
if (($irq + $dpc) -gt 5) { Flag "$t (normal under 2-3%) - driver/hardware issue (often ACPI/battery, network or storage driver); check with LatencyMon" }
|
|
else { Ok $t }
|
|
}
|
|
if ($perf -ne $null) {
|
|
$mhz = [math]::Round($cpu.MaxClockSpeed * $perf / 100)
|
|
$t = "CPU running at $perf% of base clock (~$mhz MHz of $($cpu.MaxClockSpeed) MHz)"
|
|
if ($perf -lt 60 -and $load -ge 30) { Flag "$t while busy - THROTTLED (power / battery / charger / thermal limit)" }
|
|
elseif ($perf -lt 60) { Info "$t - CPU mostly idle, low clock is normal here; see load test" }
|
|
else { Ok $t }
|
|
} else { Info 'Performance counters unavailable' }
|
|
|
|
$top = foreach ($p in $p2) {
|
|
if ($p.Id -eq 0) { continue }
|
|
$a = $p1[$p.Id]; $b = $p.TotalProcessorTime.TotalSeconds
|
|
if ($a -ne $null -and $b -ne $null) {
|
|
[pscustomobject]@{
|
|
Process = $p.ProcessName
|
|
PID = $p.Id
|
|
'CPU%' = [math]::Round(($b - $a) / $sec / $n * 100, 1)
|
|
MemMB = [math]::Round($p.WorkingSet64 / 1MB)
|
|
}
|
|
}
|
|
}
|
|
$top = $top | Sort-Object 'CPU%' -Descending | Select-Object -First 10
|
|
' Top processes (CPU% = share of whole CPU during the sample):'
|
|
($top | Format-Table -AutoSize | Out-String -Width 200).TrimEnd()
|
|
foreach ($tp in $top) {
|
|
if ($tp.'CPU%' -ge 20) { Flag "$($tp.Process) (PID $($tp.PID)) using $($tp.'CPU%')% CPU" }
|
|
if ($tp.Process -eq 'svchost' -and $tp.'CPU%' -ge 5) {
|
|
$svc = (Get-CimInstance Win32_Service -Filter "ProcessId=$($tp.PID)").Name -join ', '
|
|
Info "svchost PID $($tp.PID) hosts: $svc"
|
|
}
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
Say 'TEMPERATURE (before load)'
|
|
$tBefore = Get-Temps
|
|
if ($tBefore) { foreach ($z in $tBefore) { if ($z.C -ge 90) { Flag "Thermal zone $($z.Zone) at $($z.C) C at current use - check fan/vents" } else { Info "Thermal zone $($z.Zone) : $($z.C) C" } } }
|
|
else { Info 'Not exposed by firmware - use the OEM tool (Acer Care Center / Dell SupportAssist / Lenovo Vantage / HP Support Assistant)' }
|
|
|
|
# ---------------------------------------------------------------------------
|
|
if ($NoLoad) { Say 'CPU LOAD TEST'; Info 'Skipped (-NoLoad)' }
|
|
else {
|
|
Say "CPU LOAD TEST ($LoadSec s, 100% on all $n threads, $src)"
|
|
$stopAt = (Get-Date).AddSeconds($LoadSec)
|
|
$burn = { param($t) $x = 0.0; while ((Get-Date) -lt $t) { for ($j = 0; $j -lt 200000; $j++) { $x += [math]::Sqrt($j) } } }
|
|
$workers = foreach ($i in 1..$n) {
|
|
$ps = [powershell]::Create().AddScript($burn).AddArgument($stopAt)
|
|
[pscustomobject]@{ PS = $ps; H = $ps.BeginInvoke() }
|
|
}
|
|
Start-Sleep -Seconds 5
|
|
$lc = Get-Counter -Counter '\Processor(_Total)\% Processor Time','\Processor Information(_Total)\% Processor Performance','\Processor Information(_Total)\% Performance Limit' -SampleInterval 1 -MaxSamples ($LoadSec - 8)
|
|
$bsL = Get-CimInstance -Namespace root\wmi -ClassName BatteryStatus | Select-Object -First 1
|
|
$tAfter = Get-Temps
|
|
foreach ($w in $workers) { try { $w.PS.EndInvoke($w.H) | Out-Null } catch {}; $w.PS.Dispose() }
|
|
|
|
$cs2 = $lc.CounterSamples
|
|
$lLoad = AvgC $cs2 '*% processor time'
|
|
$lPerf = AvgC $cs2 '*% processor performance'
|
|
$lLim = AvgC $cs2 '*% performance limit'
|
|
$minP = ($cs2 | Where-Object { $_.Path -like '*% processor performance' } | Measure-Object CookedValue -Minimum).Minimum
|
|
if ($lPerf -ne $null) {
|
|
$avgMHz = [math]::Round($cpu.MaxClockSpeed * $lPerf / 100)
|
|
$minMHz = [math]::Round($cpu.MaxClockSpeed * $minP / 100)
|
|
Info "Load reached $lLoad%; speed avg ~$avgMHz MHz (lowest ~$minMHz MHz) = $lPerf% of base $($cpu.MaxClockSpeed) MHz"
|
|
if ($lLim -ne $null) { Info "Windows performance limit: $lLim% (100 = not limited)" }
|
|
if ($avgMHz -lt 1000) { Flag "SEVERE throttling under full load $src (~$avgMHz MHz) - typical of failed battery/charger (BD PROCHOT) or overheating" }
|
|
elseif ($lPerf -lt 100) { Flag "CPU below base clock under full load $src (~$avgMHz MHz) - power or thermal limit" }
|
|
else { Ok "CPU holds ~$avgMHz MHz under full load $src - no throttling" }
|
|
if ($lLim -ne $null -and $lLim -lt 90) { Flag "Windows reports performance limited to $lLim% under load $src (power mode, power or thermal limit)" }
|
|
if ($lLoad -ne $null -and $lLoad -lt 80) { Info "Load test only reached $lLoad% - another limit or security tool may be restricting it" }
|
|
} else { Info 'Performance counters unavailable - load test result not measurable' }
|
|
if ($bsL -and $bsL.PowerOnline -and $bsL.DischargeRate -gt 0) {
|
|
Flag "Battery DRAINING while plugged in under load ($($bsL.DischargeRate) mW) - adapter too weak or faulty; check charger wattage"
|
|
}
|
|
foreach ($z in $tAfter) {
|
|
if ($z.C -ge 95) { Flag "Temp after load $($z.Zone) at $($z.C) C - cooling problem (fan/vents/thermal paste)" }
|
|
else { Info "Temp after load $($z.Zone) : $($z.C) C" }
|
|
}
|
|
if ($tBefore -and $tAfter -and ((($tBefore | ForEach-Object C) -join ',') -eq (($tAfter | ForEach-Object C) -join ','))) {
|
|
Info 'Temperature did not change under load - static ACPI zone, not a live CPU sensor; use HWiNFO or Acer Care Center for real temps'
|
|
}
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
Say 'EVENT LOG (last 14 days)'
|
|
$since = (Get-Date).AddDays(-14)
|
|
$e37 = Get-WinEvent -FilterHashtable @{LogName='System'; ProviderName='Microsoft-Windows-Kernel-Processor-Power'; Id=37; StartTime=$since}
|
|
$c37 = Cnt $e37
|
|
if ($c37) { Flag "$c37 x 'CPU speed limited by system firmware' (Kernel-Processor-Power 37), latest $(@($e37)[0].TimeCreated) - typical of battery/charger/thermal limiting" }
|
|
else { Ok 'No firmware CPU-limit events (ID 37)' }
|
|
|
|
$e41 = Get-WinEvent -FilterHashtable @{LogName='System'; ProviderName='Microsoft-Windows-Kernel-Power'; Id=41; StartTime=$since}
|
|
$c41 = Cnt $e41
|
|
if ($c41) {
|
|
Flag "$c41 unexpected shutdown(s) (Kernel-Power 41) - causes below"
|
|
foreach ($e in @($e41) | Select-Object -First 10) {
|
|
$ed = @{}
|
|
foreach ($node in ([xml]$e.ToXml()).Event.EventData.Data) { $ed[$node.Name] = $node.'#text' }
|
|
$bc = [int64]$ed['BugcheckCode']
|
|
if ($bc -ne 0) { $why = 'system crash (bugcheck 0x{0:X}) - software/driver' -f $bc }
|
|
elseif ($ed['LongPowerButtonPressDetected'] -eq 'true' -or [int64]$ed['PowerButtonTimestamp'] -ne 0) { $why = 'power button held - forced off by user (likely hang)' }
|
|
elseif ($ed['SleepInProgress'] -and $ed['SleepInProgress'] -ne '0' -and $ed['SleepInProgress'] -ne 'false') { $why = 'lost power while sleeping/waking - battery or power' }
|
|
else { $why = 'sudden power loss, no crash, no button - battery/adapter suspect' }
|
|
Info ('{0} {1}' -f $e.TimeCreated, $why)
|
|
}
|
|
} else { Ok 'No unexpected shutdowns (ID 41)' }
|
|
|
|
$wh = Get-WinEvent -FilterHashtable @{LogName='System'; ProviderName='Microsoft-Windows-WHEA-Logger'; StartTime=$since}
|
|
$cw = Cnt $wh
|
|
if ($cw) { Flag "$cw hardware error event(s) (WHEA-Logger)" } else { Ok 'No WHEA hardware errors' }
|
|
|
|
# ---------------------------------------------------------------------------
|
|
Say 'MEMORY AND DISK'
|
|
$totGB = [math]::Round($os.TotalVisibleMemorySize / 1MB, 1)
|
|
$freeGB = [math]::Round($os.FreePhysicalMemory / 1MB, 1)
|
|
$t = "RAM free $freeGB GB of $totGB GB"
|
|
if ($freeGB -lt $totGB * 0.1) { Flag "Low memory: $t" } else { Ok $t }
|
|
$dk = Get-CimInstance Win32_LogicalDisk -Filter "DeviceID='C:'"
|
|
$t = "C: free $([math]::Round($dk.FreeSpace/1GB,1)) GB of $([math]::Round($dk.Size/1GB,1)) GB"
|
|
if ($dk.FreeSpace -lt $dk.Size * 0.1) { Flag "Low disk space: $t" } else { Ok $t }
|
|
Get-PhysicalDisk | ForEach-Object {
|
|
$t = "Disk $($_.FriendlyName): $($_.MediaType), health $($_.HealthStatus)"
|
|
if ($_.HealthStatus -ne 'Healthy') { Flag $t } else { Ok $t }
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
Say "SUMMARY - $PC ($src)"
|
|
if ($Flags.Count) { $Flags | ForEach-Object { " - $_" } } else { ' No issues flagged.' }
|
|
" Report saved: $Out"
|
|
} | Tee-Object -FilePath $Out
|