first commit
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
# Laptop Battery Testing Scripts
|
||||
|
||||
PowerShell scripts to stress-test laptop battery drain under a controlled synthetic CPU load,
|
||||
with all power-saving features (including hibernate-on-low-battery) disabled during the test,
|
||||
and a companion script to restore the original settings afterward.
|
||||
|
||||
## Files
|
||||
|
||||
| Script | Purpose |
|
||||
|---|---|
|
||||
| [Test-BatteryDrain.ps1](Test-BatteryDrain.ps1) | Disables power-saving/hibernate settings, generates ~30% CPU load, logs battery drain to CSV. |
|
||||
| [Restore-PowerSettings.ps1](Restore-PowerSettings.ps1) | Reverts the power scheme and hibernate settings changed by the test script. |
|
||||
|
||||
Both scripts self-elevate (relaunch as Administrator) if not already running elevated, since
|
||||
`powercfg` changes require admin rights.
|
||||
|
||||
## Test-BatteryDrain.ps1
|
||||
|
||||
### What it does
|
||||
|
||||
1. Backs up the currently active power scheme GUID and hibernate on/off state to
|
||||
`PowerSettingsBackup.json` (used later by the restore script).
|
||||
2. Switches to the **Ultimate Performance** plan (duplicated if not already present) or
|
||||
**High performance** as a fallback, so Windows doesn't throttle the CPU during the test.
|
||||
3. Disables, on both AC and battery (DC):
|
||||
- Monitor, disk, standby, and hibernate timeouts.
|
||||
- Standby/hibernate idle behavior at the sleep-subgroup level.
|
||||
- Low/critical battery actions (set to "Do Nothing" instead of sleep/hibernate/shutdown).
|
||||
- Low/critical battery notification flyouts.
|
||||
- USB selective suspend.
|
||||
- PCI Express link state power management (ASPM).
|
||||
- Adaptive brightness and display dimming.
|
||||
4. Runs `powercfg /hibernate off` to fully disable hibernation (removes `hiberfil.sys`).
|
||||
5. Calls `SetThreadExecutionState` to keep the system awake/display on for the life of the
|
||||
script, as a redundant safety net.
|
||||
6. Starts one background job per logical CPU core, each duty-cycling busy/sleep periods to
|
||||
approximate the requested overall CPU load (`-TargetLoadPercent`, default 30%).
|
||||
7. Samples battery percentage/charging status every `-SampleIntervalSeconds` and appends each
|
||||
sample to a CSV log (`Timestamp,PercentRemaining,Status,Charging,EstimatedRunTimeMinutes`).
|
||||
8. Runs for `-DurationMinutes` (or indefinitely if `0`, until Ctrl+C), then stops the CPU load
|
||||
jobs, clears the stay-awake flag, and prints a summary (start/end %, elapsed time, average
|
||||
drain rate in %/hour).
|
||||
|
||||
### Parameters
|
||||
|
||||
| Parameter | Default | Description |
|
||||
|---|---|---|
|
||||
| `-DurationMinutes` | `60` | Test length in minutes. `0` = run until Ctrl+C. |
|
||||
| `-SampleIntervalSeconds` | `30` | How often to sample/log battery status. |
|
||||
| `-TargetLoadPercent` | `30` | Approximate overall CPU load to generate (0-100). |
|
||||
| `-LogPath` | `BatteryDrainLog_<timestamp>.csv` next to the script | CSV log output path. |
|
||||
| `-StateFilePath` | `PowerSettingsBackup.json` next to the script | Where the pre-test settings backup is saved. |
|
||||
| `-SkipPowerSettingsChanges` | off | Only run the CPU load + logging; don't touch power settings. |
|
||||
|
||||
### Examples
|
||||
|
||||
```powershell
|
||||
# Default: 60 min test, ~30% CPU load, sample every 30s
|
||||
.\Test-BatteryDrain.ps1
|
||||
|
||||
# 2 hour test, ~25% load, sample every minute
|
||||
.\Test-BatteryDrain.ps1 -DurationMinutes 120 -TargetLoadPercent 25 -SampleIntervalSeconds 60
|
||||
|
||||
# Run until manually stopped (Ctrl+C), custom log location
|
||||
.\Test-BatteryDrain.ps1 -DurationMinutes 0 -LogPath D:\logs\battery.csv
|
||||
```
|
||||
|
||||
### Output
|
||||
|
||||
- A CSV log with one row per sample interval.
|
||||
- Console output with each sampled reading.
|
||||
- A final summary block with total percentage-point drop and average %/hour drain rate.
|
||||
|
||||
## Restore-PowerSettings.ps1
|
||||
|
||||
### What it does
|
||||
|
||||
1. Runs `powercfg -restoredefaultschemes` to reset Balanced/Power saver/High performance back
|
||||
to Windows factory defaults, undoing the timeout/USB/PCIe/battery-action changes.
|
||||
2. Re-activates whichever power scheme was active before the test (read from the state file).
|
||||
3. Deletes the duplicated "Ultimate Performance" scheme if the test script created one.
|
||||
4. Restores hibernation to its original on/off state (`powercfg /hibernate on|off`), defaulting
|
||||
to "on" if no state file is found.
|
||||
5. Deletes the state file once restoration is complete.
|
||||
|
||||
### Parameters
|
||||
|
||||
| Parameter | Default | Description |
|
||||
|---|---|---|
|
||||
| `-StateFilePath` | `PowerSettingsBackup.json` next to the script | Path to the state file written by `Test-BatteryDrain.ps1`. |
|
||||
|
||||
### Example
|
||||
|
||||
```powershell
|
||||
.\Restore-PowerSettings.ps1
|
||||
```
|
||||
|
||||
## Typical Workflow
|
||||
|
||||
```powershell
|
||||
# 1. Unplug the laptop charger.
|
||||
# 2. Run the drain test (e.g. 2 hours at 30% load):
|
||||
.\Test-BatteryDrain.ps1 -DurationMinutes 120 -TargetLoadPercent 30
|
||||
|
||||
# 3. Review the generated BatteryDrainLog_*.csv for drain rate / behavior.
|
||||
|
||||
# 4. Restore normal power behavior:
|
||||
.\Restore-PowerSettings.ps1
|
||||
```
|
||||
|
||||
## Notes & Caveats
|
||||
|
||||
- Requires Administrator rights (scripts self-elevate via UAC prompt).
|
||||
- `Test-BatteryDrain.ps1` does **not** auto-restore settings when it finishes or is interrupted
|
||||
with Ctrl+C — always run `Restore-PowerSettings.ps1` afterward to revert the system to its
|
||||
normal power-saving behavior.
|
||||
- If `Test-BatteryDrain.ps1` is interrupted before it finishes, the CPU load jobs and
|
||||
stay-awake flag are cleared via its `finally` block, but the power scheme changes remain in
|
||||
effect until you run the restore script.
|
||||
- The synthetic CPU load is an approximation (each core independently duty-cycles busy/idle
|
||||
time); actual measured CPU usage may vary slightly from `-TargetLoadPercent` depending on
|
||||
system background activity and CPU frequency scaling.
|
||||
- `powercfg -restoredefaultschemes` resets **all** built-in schemes to factory defaults,
|
||||
including any manual customizations you made to them outside of this test — not just the
|
||||
ones changed by `Test-BatteryDrain.ps1`.
|
||||
@@ -0,0 +1,99 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Reverts the power-saving/hibernate changes made by Test-BatteryDrain.ps1.
|
||||
|
||||
.DESCRIPTION
|
||||
1. Re-launches itself elevated if not running as Administrator (powercfg changes require admin).
|
||||
2. Resets all built-in power schemes (Balanced, Power saver, High performance) to their Windows
|
||||
factory defaults, which undoes the timeout/USB/PCIe/battery-action tweaks applied by the test
|
||||
script.
|
||||
3. Removes the "Ultimate Performance" scheme duplicate if the test script created one.
|
||||
4. Restores hibernation to its original on/off state, and re-activates whichever power scheme was
|
||||
active before the test ran (both read from the state file saved by Test-BatteryDrain.ps1).
|
||||
|
||||
.PARAMETER StateFilePath
|
||||
Path to the JSON state file written by Test-BatteryDrain.ps1. Defaults to
|
||||
.\PowerSettingsBackup.json next to this script.
|
||||
|
||||
.EXAMPLE
|
||||
.\Restore-PowerSettings.ps1
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$StateFilePath = $(Join-Path -Path $PSScriptRoot -ChildPath 'PowerSettingsBackup.json')
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
#region Elevation check
|
||||
function Test-IsAdmin {
|
||||
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
|
||||
$principal = New-Object Security.Principal.WindowsPrincipal($identity)
|
||||
return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
|
||||
}
|
||||
|
||||
if (-not (Test-IsAdmin)) {
|
||||
Write-Host "Not running elevated - relaunching as Administrator..." -ForegroundColor Yellow
|
||||
Start-Process -FilePath 'powershell.exe' -ArgumentList @(
|
||||
'-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', "`"$PSCommandPath`"",
|
||||
'-StateFilePath', "`"$StateFilePath`""
|
||||
) -Verb RunAs
|
||||
exit
|
||||
}
|
||||
#endregion
|
||||
|
||||
Write-Host "Restoring default power schemes (undoes timeout/USB/PCIe/battery-action changes)..." -ForegroundColor Cyan
|
||||
powercfg -restoredefaultschemes | Out-Null
|
||||
|
||||
# Remove the Ultimate Performance duplicate scheme created by Test-BatteryDrain.ps1, if present.
|
||||
$ultimateGuid = (powercfg /list | Select-String -Pattern 'Ultimate Performance' | ForEach-Object {
|
||||
if ($_ -match '([0-9a-fA-F-]{36})') { $Matches[1] }
|
||||
}) | Select-Object -First 1
|
||||
|
||||
$state = $null
|
||||
if (Test-Path $StateFilePath) {
|
||||
$state = Get-Content -Path $StateFilePath -Raw | ConvertFrom-Json
|
||||
} else {
|
||||
Write-Host "No state file found at $StateFilePath - will restore hibernate to 'on' and leave the active scheme as Balanced." -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
# Re-activate whichever scheme was active before the test, so we don't strand the machine on the
|
||||
# duplicated Ultimate Performance scheme right before deleting it.
|
||||
if ($state -and $state.OriginalSchemeGuid) {
|
||||
try {
|
||||
powercfg /setactive $state.OriginalSchemeGuid | Out-Null
|
||||
Write-Host "Restored original active power scheme ($($state.OriginalSchemeGuid))." -ForegroundColor Green
|
||||
} catch {
|
||||
Write-Host "Could not restore original scheme GUID; falling back to Balanced." -ForegroundColor Yellow
|
||||
powercfg /setactive SCHEME_BALANCED | Out-Null
|
||||
}
|
||||
} else {
|
||||
powercfg /setactive SCHEME_BALANCED | Out-Null
|
||||
Write-Host "Active plan set to Balanced (default)." -ForegroundColor Green
|
||||
}
|
||||
|
||||
if ($ultimateGuid) {
|
||||
try {
|
||||
powercfg /delete $ultimateGuid | Out-Null
|
||||
Write-Host "Removed the Ultimate Performance scheme created for the test." -ForegroundColor Green
|
||||
} catch {
|
||||
Write-Host "Could not remove Ultimate Performance scheme (it may still be active); skipping." -ForegroundColor Yellow
|
||||
}
|
||||
}
|
||||
|
||||
# Restore hibernate to its original state (defaults to "on" if no state file is available).
|
||||
$hibernateEnabled = if ($state) { [bool]$state.HibernateEnabled } else { $true }
|
||||
if ($hibernateEnabled) {
|
||||
powercfg /hibernate on | Out-Null
|
||||
Write-Host "Hibernate re-enabled." -ForegroundColor Green
|
||||
} else {
|
||||
powercfg /hibernate off | Out-Null
|
||||
Write-Host "Hibernate left disabled (it was already off before the test)." -ForegroundColor Green
|
||||
}
|
||||
|
||||
if (Test-Path $StateFilePath) {
|
||||
Remove-Item -Path $StateFilePath -Force
|
||||
}
|
||||
|
||||
Write-Host "Power settings restored to their pre-test state." -ForegroundColor Yellow
|
||||
@@ -0,0 +1,353 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Disables Windows power-saving features (including hibernate on low battery),
|
||||
applies a synthetic CPU load, and logs battery drain over time.
|
||||
|
||||
.DESCRIPTION
|
||||
1. Re-launches itself elevated if not running as Administrator (powercfg changes require admin).
|
||||
2. Disables sleep/hibernate/monitor timeouts, USB selective suspend, PCI Express link power
|
||||
management, and the "hibernate/shutdown on critical/low battery" actions, on both AC and DC.
|
||||
3. Sets the active power plan to High performance (or Ultimate Performance if available).
|
||||
4. Starts a background synthetic load across all logical processors targeting ~30% CPU usage
|
||||
(configurable via -TargetLoadPercent).
|
||||
5. Samples battery percentage/status every -SampleIntervalSeconds and appends to a CSV log
|
||||
until -DurationMinutes elapses (or indefinitely with -DurationMinutes 0, stop with Ctrl+C).
|
||||
6. Prints a summary (start/end charge, elapsed time, average drain rate %/hour).
|
||||
|
||||
.PARAMETER DurationMinutes
|
||||
How long to run the test. 0 = run until Ctrl+C is pressed.
|
||||
|
||||
.PARAMETER SampleIntervalSeconds
|
||||
How often to sample/log the battery status.
|
||||
|
||||
.PARAMETER TargetLoadPercent
|
||||
Approximate overall CPU load to generate (0-100). Default 30.
|
||||
|
||||
.PARAMETER LogPath
|
||||
Path to the CSV log file. Defaults to .\BatteryDrainLog_<timestamp>.csv next to the script.
|
||||
|
||||
.PARAMETER SkipPowerSettingsChanges
|
||||
Skip modifying power settings; only run the load + logging.
|
||||
|
||||
.EXAMPLE
|
||||
.\Test-BatteryDrain.ps1 -DurationMinutes 120 -TargetLoadPercent 30
|
||||
|
||||
.NOTES
|
||||
Restoring defaults: the script does not automatically revert power settings it changes.
|
||||
Run `powercfg /restoredefaultschemes` (drastic) or re-enable individual settings manually
|
||||
if you want the previous behavior back.
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[int]$DurationMinutes = 60,
|
||||
[int]$SampleIntervalSeconds = 30,
|
||||
[ValidateRange(0, 100)]
|
||||
[int]$TargetLoadPercent = 30,
|
||||
[string]$LogPath = $(Join-Path -Path $PSScriptRoot -ChildPath ("BatteryDrainLog_{0:yyyyMMdd_HHmmss}.csv" -f (Get-Date))),
|
||||
[string]$StateFilePath = $(Join-Path -Path $PSScriptRoot -ChildPath 'PowerSettingsBackup.json'),
|
||||
[switch]$SkipPowerSettingsChanges
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
#region Elevation check
|
||||
function Test-IsAdmin {
|
||||
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
|
||||
$principal = New-Object Security.Principal.WindowsPrincipal($identity)
|
||||
return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
|
||||
}
|
||||
|
||||
if (-not (Test-IsAdmin)) {
|
||||
Write-Host "Not running elevated - relaunching as Administrator..." -ForegroundColor Yellow
|
||||
$argList = @(
|
||||
'-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', "`"$PSCommandPath`"",
|
||||
'-DurationMinutes', $DurationMinutes,
|
||||
'-SampleIntervalSeconds', $SampleIntervalSeconds,
|
||||
'-TargetLoadPercent', $TargetLoadPercent,
|
||||
'-LogPath', "`"$LogPath`"",
|
||||
'-StateFilePath', "`"$StateFilePath`""
|
||||
)
|
||||
if ($SkipPowerSettingsChanges) { $argList += '-SkipPowerSettingsChanges' }
|
||||
Start-Process -FilePath 'powershell.exe' -ArgumentList $argList -Verb RunAs
|
||||
exit
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Power settings
|
||||
function Save-PowerSettingsState {
|
||||
param([string]$Path)
|
||||
|
||||
# Original active scheme GUID (strip the trailing description in parentheses).
|
||||
$activeLine = (powercfg /getactivescheme) -join ''
|
||||
$activeGuid = if ($activeLine -match '([0-9a-fA-F-]{36})') { $Matches[1] } else { $null }
|
||||
|
||||
# Hibernate is considered enabled if hiberfil.sys exists / powercfg /a doesn't list it as unavailable due to being off.
|
||||
$hibernateEnabled = (powercfg /a) -join "`n" -notmatch 'Hibernation has not been enabled'
|
||||
|
||||
$state = [PSCustomObject]@{
|
||||
OriginalSchemeGuid = $activeGuid
|
||||
HibernateEnabled = [bool]$hibernateEnabled
|
||||
SavedAt = (Get-Date).ToString('o')
|
||||
}
|
||||
$state | ConvertTo-Json | Out-File -FilePath $Path -Encoding UTF8
|
||||
Write-Host "Saved original power settings to $Path (used by Restore-PowerSettings.ps1)." -ForegroundColor DarkGray
|
||||
}
|
||||
|
||||
function Set-PowerSettingsForBatteryTest {
|
||||
Write-Host "Configuring power plan to prevent sleep/hibernate/screen-off and low-battery actions..." -ForegroundColor Cyan
|
||||
|
||||
Save-PowerSettingsState -Path $StateFilePath
|
||||
|
||||
# Try to use Ultimate/High performance plan so Windows itself doesn't throttle or sleep.
|
||||
$schemes = powercfg /list
|
||||
$ultimateGuid = ($schemes | Select-String -Pattern 'Ultimate Performance' | ForEach-Object {
|
||||
($_ -match '([0-9a-fA-F-]{36})') | Out-Null; $Matches[1]
|
||||
}) | Select-Object -First 1
|
||||
|
||||
if (-not $ultimateGuid) {
|
||||
try {
|
||||
$dup = powercfg -duplicatescheme e9a42b02-d5df-448d-aa00-03f14749eb61 2>$null
|
||||
$ultimateGuid = ($dup | Select-String -Pattern '([0-9a-fA-F-]{36})' | ForEach-Object { $Matches[1] }) | Select-Object -First 1
|
||||
} catch { }
|
||||
}
|
||||
|
||||
if ($ultimateGuid) {
|
||||
powercfg /setactive $ultimateGuid | Out-Null
|
||||
Write-Host "Active plan set to Ultimate Performance." -ForegroundColor Green
|
||||
} else {
|
||||
# SCHEME_MIN = High performance
|
||||
powercfg /setactive SCHEME_MIN | Out-Null
|
||||
Write-Host "Active plan set to High performance." -ForegroundColor Green
|
||||
}
|
||||
|
||||
# Disable timeouts for sleep, hibernate, monitor, disk - both AC (ac) and DC (battery, dc).
|
||||
$targets = @('monitor-timeout-ac', 'monitor-timeout-dc',
|
||||
'disk-timeout-ac', 'disk-timeout-dc',
|
||||
'standby-timeout-ac', 'standby-timeout-dc',
|
||||
'hibernate-timeout-ac', 'hibernate-timeout-dc')
|
||||
foreach ($t in $targets) {
|
||||
powercfg /change $t 0 | Out-Null
|
||||
}
|
||||
|
||||
# Disable "Allow hybrid sleep" and standby/hibernate at the GUID level (belt & suspenders).
|
||||
# SUB_SLEEP settings
|
||||
$SUB_SLEEP = '238c9fa8-0aad-41ed-83f4-97be242c8f20'
|
||||
$STANDBYIDLE = '29f6c1db-86da-48c5-9fdb-f2b67b1f44da'
|
||||
$HIBERNATEIDLE = '9d7815a6-7ee4-497e-8888-515a05f02364'
|
||||
foreach ($mode in @('/setacvalueindex', '/setdcvalueindex')) {
|
||||
powercfg $mode SCHEME_CURRENT $SUB_SLEEP $STANDBYIDLE 0 | Out-Null
|
||||
powercfg $mode SCHEME_CURRENT $SUB_SLEEP $HIBERNATEIDLE 0 | Out-Null
|
||||
}
|
||||
|
||||
# Disable low/critical battery actions (hibernate/shutdown) - the actual ask: no hibernate on low battery.
|
||||
$SUB_BATTERY = 'e73a048d-bf27-4f12-9731-8b2076e8891f'
|
||||
$BATACTIONCRIT = '637ea02f-bbcb-4015-8e2c-a1c793b9d808' # Critical battery action
|
||||
$BATACTIONLOW = 'd8742dcb-3e6a-4b3c-b3fe-374623cdcf06' # Low battery action
|
||||
foreach ($mode in @('/setacvalueindex', '/setdcvalueindex')) {
|
||||
# 0 = Do Nothing, 1 = Sleep, 2 = Hibernate, 3 = Shut Down
|
||||
powercfg $mode SCHEME_CURRENT $SUB_BATTERY $BATACTIONCRIT 0 | Out-Null
|
||||
powercfg $mode SCHEME_CURRENT $SUB_BATTERY $BATACTIONLOW 0 | Out-Null
|
||||
}
|
||||
|
||||
# Turn off notifications tied to low battery flyout as well (0 = off).
|
||||
$BATFLAGSCRIT = '5dbb7c9f-38e9-40d2-9749-4f8a0e9f640f'
|
||||
$BATFLAGSLOW = 'bcded951-187b-4d05-bccc-f7e51960c258'
|
||||
foreach ($mode in @('/setacvalueindex', '/setdcvalueindex')) {
|
||||
powercfg $mode SCHEME_CURRENT $SUB_BATTERY $BATFLAGSCRIT 0 2>$null | Out-Null
|
||||
powercfg $mode SCHEME_CURRENT $SUB_BATTERY $BATFLAGSLOW 0 2>$null | Out-Null
|
||||
}
|
||||
|
||||
# Explicitly disable OS-level hibernate (removes hiberfil.sys, guarantees no hibernate trigger).
|
||||
powercfg /hibernate off | Out-Null
|
||||
|
||||
# Disable USB selective suspend and PCI Express link state power management (both drain-reducers, but they
|
||||
# can throttle performance under sustained load, which we don't want while stress-testing).
|
||||
$SUB_USB = '2a737441-1930-4402-8d77-b2bebba308a3'
|
||||
$USBSUSPEND = '48e6b7a6-50f5-4782-a5d4-53bb8f07e226'
|
||||
$SUB_PCIE = '501a4d13-42af-4429-9fd1-a8218c268e20'
|
||||
$PCIE_ASPM = 'ee12f906-d277-404b-b6da-e5fa1a576df5'
|
||||
foreach ($mode in @('/setacvalueindex', '/setdcvalueindex')) {
|
||||
powercfg $mode SCHEME_CURRENT $SUB_USB $USBSUSPEND 0 2>$null | Out-Null
|
||||
powercfg $mode SCHEME_CURRENT $SUB_PCIE $PCIE_ASPM 0 2>$null | Out-Null
|
||||
}
|
||||
|
||||
# Disable adaptive brightness / dimming on battery.
|
||||
$SUB_VIDEO = '7516b95f-f776-4464-8c53-06167f40cc99'
|
||||
$ADAPTBRIGHT = 'fbd9aa66-9553-4097-ba44-ed6e9d65eab8'
|
||||
$DIMDISPLAY = '17aaa29b-8b43-4b94-aafe-35f64daaf1ee'
|
||||
foreach ($mode in @('/setacvalueindex', '/setdcvalueindex')) {
|
||||
powercfg $mode SCHEME_CURRENT $SUB_VIDEO $ADAPTBRIGHT 0 2>$null | Out-Null
|
||||
powercfg $mode SCHEME_CURRENT $SUB_VIDEO $DIMDISPLAY 0 2>$null | Out-Null
|
||||
}
|
||||
|
||||
powercfg /setactive SCHEME_CURRENT | Out-Null
|
||||
|
||||
# Prevent Windows from turning off the display/sleeping via the SendMessage/SetThreadExecutionState API too,
|
||||
# as a redundant safety net for the duration of this script (handled later via a background job).
|
||||
|
||||
Write-Host "Power settings applied." -ForegroundColor Green
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Keep system awake (SetThreadExecutionState) for the life of the script
|
||||
Add-Type -Namespace Native -Name Power -MemberDefinition @'
|
||||
[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
|
||||
public static extern uint SetThreadExecutionState(uint esFlags);
|
||||
'@
|
||||
|
||||
$ES_CONTINUOUS = 0x80000000
|
||||
$ES_SYSTEM_REQUIRED = 0x00000001
|
||||
$ES_DISPLAY_REQUIRED = 0x00000002
|
||||
|
||||
function Set-StayAwake {
|
||||
[Native.Power]::SetThreadExecutionState($ES_CONTINUOUS -bor $ES_SYSTEM_REQUIRED -bor $ES_DISPLAY_REQUIRED) | Out-Null
|
||||
}
|
||||
|
||||
function Clear-StayAwake {
|
||||
[Native.Power]::SetThreadExecutionState($ES_CONTINUOUS) | Out-Null
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Synthetic CPU load (~TargetLoadPercent across all logical cores)
|
||||
function Start-CpuLoad {
|
||||
param([int]$Percent, [int]$Cores)
|
||||
|
||||
$jobs = @()
|
||||
for ($i = 0; $i -lt $Cores; $i++) {
|
||||
$jobs += Start-Job -ScriptBlock {
|
||||
param($pct)
|
||||
$busyMs = [double]$pct
|
||||
$idleMs = 100.0 - $busyMs
|
||||
$sw = [System.Diagnostics.Stopwatch]::new()
|
||||
while ($true) {
|
||||
$sw.Restart()
|
||||
while ($sw.Elapsed.TotalMilliseconds -lt $busyMs) {
|
||||
# Busy-spin to consume CPU
|
||||
[Math]::Sqrt(12345.6789) | Out-Null
|
||||
}
|
||||
if ($idleMs -gt 0) {
|
||||
Start-Sleep -Milliseconds $idleMs
|
||||
}
|
||||
}
|
||||
} -ArgumentList $Percent
|
||||
}
|
||||
return $jobs
|
||||
}
|
||||
|
||||
function Stop-CpuLoad {
|
||||
param($Jobs)
|
||||
if ($Jobs) {
|
||||
$Jobs | Stop-Job -PassThru | Remove-Job -Force
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Battery sampling
|
||||
function Get-BatteryInfo {
|
||||
$battery = Get-CimInstance -ClassName Win32_Battery -ErrorAction SilentlyContinue | Select-Object -First 1
|
||||
if (-not $battery) {
|
||||
return [PSCustomObject]@{
|
||||
Timestamp = Get-Date
|
||||
PercentRemaining = $null
|
||||
Status = 'No battery detected'
|
||||
Charging = $null
|
||||
EstimatedRunTime = $null
|
||||
}
|
||||
}
|
||||
|
||||
$chargingStatus = switch ($battery.BatteryStatus) {
|
||||
1 { 'Discharging' }
|
||||
2 { 'AC Power (Charged)' }
|
||||
3 { 'Fully Charged' }
|
||||
4 { 'Low' }
|
||||
5 { 'Critical' }
|
||||
6 { 'Charging' }
|
||||
7 { 'Charging High' }
|
||||
8 { 'Charging Low' }
|
||||
9 { 'Charging Critical' }
|
||||
10 { 'Undefined' }
|
||||
11 { 'Partially Charged' }
|
||||
default { "Unknown ($($battery.BatteryStatus))" }
|
||||
}
|
||||
|
||||
[PSCustomObject]@{
|
||||
Timestamp = Get-Date
|
||||
PercentRemaining = $battery.EstimatedChargeRemaining
|
||||
Status = $chargingStatus
|
||||
Charging = ($battery.BatteryStatus -in 2, 6, 7, 8, 9)
|
||||
EstimatedRunTime = $battery.EstimatedRunTime
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Main
|
||||
try {
|
||||
if (-not $SkipPowerSettingsChanges) {
|
||||
Set-PowerSettingsForBatteryTest
|
||||
} else {
|
||||
Write-Host "Skipping power settings changes (per -SkipPowerSettingsChanges)." -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
Set-StayAwake
|
||||
|
||||
$cores = [Environment]::ProcessorCount
|
||||
Write-Host "Starting synthetic CPU load targeting ~$TargetLoadPercent% across $cores logical processors..." -ForegroundColor Cyan
|
||||
$loadJobs = Start-CpuLoad -Percent $TargetLoadPercent -Cores $cores
|
||||
|
||||
if (-not (Test-Path (Split-Path -Path $LogPath -Parent))) {
|
||||
New-Item -ItemType Directory -Path (Split-Path -Path $LogPath -Parent) -Force | Out-Null
|
||||
}
|
||||
"Timestamp,PercentRemaining,Status,Charging,EstimatedRunTimeMinutes" | Out-File -FilePath $LogPath -Encoding UTF8
|
||||
|
||||
Write-Host "Logging battery status every $SampleIntervalSeconds s to $LogPath" -ForegroundColor Cyan
|
||||
if ($DurationMinutes -gt 0) {
|
||||
Write-Host "Test will run for $DurationMinutes minute(s). Press Ctrl+C to stop early." -ForegroundColor Cyan
|
||||
} else {
|
||||
Write-Host "Test will run indefinitely. Press Ctrl+C to stop." -ForegroundColor Cyan
|
||||
}
|
||||
|
||||
$startTime = Get-Date
|
||||
$endTime = if ($DurationMinutes -gt 0) { $startTime.AddMinutes($DurationMinutes) } else { [DateTime]::MaxValue }
|
||||
$firstSample = $null
|
||||
$lastSample = $null
|
||||
|
||||
while ((Get-Date) -lt $endTime) {
|
||||
$info = Get-BatteryInfo
|
||||
if (-not $firstSample) { $firstSample = $info }
|
||||
$lastSample = $info
|
||||
|
||||
$line = "{0:o},{1},{2},{3},{4}" -f $info.Timestamp, $info.PercentRemaining, $info.Status, $info.Charging, $info.EstimatedRunTime
|
||||
Add-Content -Path $LogPath -Value $line
|
||||
|
||||
Write-Host ("[{0:HH:mm:ss}] Battery: {1}% Status: {2}" -f $info.Timestamp, $info.PercentRemaining, $info.Status)
|
||||
|
||||
Start-Sleep -Seconds $SampleIntervalSeconds
|
||||
}
|
||||
}
|
||||
finally {
|
||||
Write-Host "Stopping CPU load and restoring system idle behavior..." -ForegroundColor Cyan
|
||||
Stop-CpuLoad -Jobs $loadJobs
|
||||
Clear-StayAwake
|
||||
|
||||
if ($firstSample -and $lastSample -and $firstSample.PercentRemaining -ne $null -and $lastSample.PercentRemaining -ne $null) {
|
||||
$elapsedHours = ($lastSample.Timestamp - $firstSample.Timestamp).TotalHours
|
||||
$drop = $firstSample.PercentRemaining - $lastSample.PercentRemaining
|
||||
$rate = if ($elapsedHours -gt 0) { $drop / $elapsedHours } else { 0 }
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "===== Battery Drain Test Summary =====" -ForegroundColor Yellow
|
||||
Write-Host ("Start: {0:yyyy-MM-dd HH:mm:ss} {1}%" -f $firstSample.Timestamp, $firstSample.PercentRemaining)
|
||||
Write-Host ("End: {0:yyyy-MM-dd HH:mm:ss} {1}%" -f $lastSample.Timestamp, $lastSample.PercentRemaining)
|
||||
Write-Host ("Elapsed: {0:N2} hour(s)" -f $elapsedHours)
|
||||
Write-Host ("Total drop: {0} percentage points" -f $drop)
|
||||
Write-Host ("Average drain rate: {0:N2} %/hour" -f $rate)
|
||||
Write-Host ("Log file: {0}" -f $LogPath)
|
||||
Write-Host "=======================================" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
if (-not $SkipPowerSettingsChanges) {
|
||||
Write-Host "Run .\Restore-PowerSettings.ps1 to revert the power/hibernate settings changed by this script." -ForegroundColor Cyan
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
Reference in New Issue
Block a user