From c4dbaee0cb53918e29e66f2233edd797e0fb154c Mon Sep 17 00:00:00 2001 From: Tero Date: Fri, 28 Aug 2026 09:40:46 +0300 Subject: [PATCH] first commit --- README.md | 125 ++++++++++++++ Restore-PowerSettings.ps1 | 99 +++++++++++ Test-BatteryDrain.ps1 | 353 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 577 insertions(+) create mode 100644 README.md create mode 100644 Restore-PowerSettings.ps1 create mode 100644 Test-BatteryDrain.ps1 diff --git a/README.md b/README.md new file mode 100644 index 0000000..45e1083 --- /dev/null +++ b/README.md @@ -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_.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`. diff --git a/Restore-PowerSettings.ps1 b/Restore-PowerSettings.ps1 new file mode 100644 index 0000000..66d9994 --- /dev/null +++ b/Restore-PowerSettings.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 diff --git a/Test-BatteryDrain.ps1 b/Test-BatteryDrain.ps1 new file mode 100644 index 0000000..33e5c20 --- /dev/null +++ b/Test-BatteryDrain.ps1 @@ -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_.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