From 0a184e042080f383530b3545b63d41b21e7db072 Mon Sep 17 00:00:00 2001 From: Kristoffer Konstad Hansen Date: Tue, 28 Jul 2026 11:58:00 +0200 Subject: [PATCH 1/4] Automatic Test Mode: run until every core has a final value, persist per-core results, continue after resume (#106) - Introduce a per-core state machine (pending/testing/confirmed/unstable/ignored) that drives all Automatic Test Mode scheduling; a core is confirmed after passesToConfirmCoreValue (new setting, default 3) consecutive error-free test runs at an unchanged value and is then never tested again, in-run or after a crash/resume - Append confirmed/unstable results immediately to a permanent, human-readable results file that survives crashes and reboots - In Automatic Test Mode the run now loops until every non-ignored core is resolved; maxIterations no longer governs the ATM loop (classic mode unchanged); an internal sanity limit catches pathological cores - Persist per-core states, iteration and the remaining core order in the .automode file (atomic writes, previous generation kept as backup) so an automatic resume continues the order where it left off instead of restarting from the beginning (fixes #106) - Add knownGoodValues setting to declare trusted per-core values that are never tested and reported as user-provided (AMD only) - Fix the false "All Cores have reached the maximum Curve Optimizer value" abort: duplicate-accepting error lists were counted against a de-duplicated core count - Harden the crash/resume machinery: the adjusted value of a crashed core is persisted before any voltage is applied to the CPU after a resume; terminal results from the results file are merged back on resume so a torn state write can never cause a confirmed core to be retested; a no-progress boot counter aborts a pathological crash-reboot loop (new exit code 7); the startup helper falls back to the backup state generation on any primary read/validation failure - Fix the alternate core order builder producing duplicate/incomplete orders for odd physical-core counts (even counts unchanged) - Fix pass detection dying permanently when a stress-test log is recreated smaller after a program restart; fix corepairs resume using $_ outside a pipeline - Per-core status row (OK/CFG/MAX) in the final summary; banner and exit code now reflect the actual outcome - Document everything in default.config.ini, the ATM example configs and readme.txt Co-Authored-By: Claude Fable 5 --- .gitignore | 2 + configs/Intel.AutomaticTestMode.yCruncher.ini | 9 + configs/Ryzen.AutomaticTestMode.Start.ini | 19 +- configs/default.config.ini | 67 +- helpers/automode-startup-script.ps1 | 91 +- readme.txt | 72 + script-corecycler.ps1 | 1709 +++++++++++++++-- 7 files changed, 1798 insertions(+), 171 deletions(-) diff --git a/.gitignore b/.gitignore index 8154e83..fb6eabb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ .automode +.automode-bak +.automode-temp .updatecheck config.ini multiconfig-*.ini diff --git a/configs/Intel.AutomaticTestMode.yCruncher.ini b/configs/Intel.AutomaticTestMode.yCruncher.ini index 10b5a83..7abb5ea 100644 --- a/configs/Intel.AutomaticTestMode.yCruncher.ini +++ b/configs/Intel.AutomaticTestMode.yCruncher.ini @@ -20,6 +20,15 @@ startValues = -150 # Set the voltage offset to -150mv maxValue = 0 # Do not go above a voltage offset of 0mvv incrementBy = 10 # Adjust the voltage offset by 10mv after an error +# A core needs to complete this many test runs in a row without an error before the voltage offset +# is considered good for it +# Note that for Intel there is only a single voltage offset for all of the cores, so an adjustment +# resets this counter for every core +passesToConfirmCoreValue = 3 + +# The test runs until every core has found a good value, the "maxIterations" setting is not used +# The found values are permanently stored in the "*_automode-results.txt" file in the logs directory + # Try to automatically resume after a crash / hard reboot # For this to work properly, Auto Logon should be activated # Otherwise the computer will just idle at the logon screen after a crash diff --git a/configs/Ryzen.AutomaticTestMode.Start.ini b/configs/Ryzen.AutomaticTestMode.Start.ini index cc70238..b5eaf4b 100644 --- a/configs/Ryzen.AutomaticTestMode.Start.ini +++ b/configs/Ryzen.AutomaticTestMode.Start.ini @@ -3,7 +3,9 @@ # It sets the Curve Optimizer values to the minimum value and will make it less aggressive # step by step when an error or a crash occurs # -# This took around 8 hours for my 5900X starting at -30 Curve Optimizer until 10 iterations had completed +# The test now runs until every core has found a good value (or until the maxValue has been reached +# for a core), so expect this to take many hours +# The found values are permanently stored in the "*_automode-results.txt" file in the logs directory # # Author: sp00n @@ -13,7 +15,8 @@ stressTestProgram = YCRUNCHER runtimePerCore = auto coreTestOrder = Default numberOfThreads = 1 -maxIterations = 10 + +# Note: maxIterations is not used in the Automatic Test Mode [yCruncher] @@ -32,6 +35,18 @@ maxValue = 0 # Do not go above Curve Optimizer value of 0 incrementBy = 1 # Adjust the Curve Optimizer value by 1 after an error +# A core needs to complete this many test runs in a row without an error before its Curve Optimizer +# value is considered good and the core is not tested anymore +# Higher values give you more confidence, but the test will take proportionally longer +passesToConfirmCoreValue = 3 + + +# If you already know the good value for some of the cores, you can enter them here +# These cores will then not be tested at all +# You can find a ready to use line at the end of the "*_automode-results.txt" file of a previous run +#knownGoodValues = 3:-25, 7:-18 + + # Set the Curve Optimizer value only for the currently tested core # All other cores will be set to 0, to avoid any possible interference setVoltageOnlyForTestedCore = 1 diff --git a/configs/default.config.ini b/configs/default.config.ini index 6745189..b08fa68 100644 --- a/configs/default.config.ini +++ b/configs/default.config.ini @@ -148,6 +148,9 @@ assignBothVirtualCoresForSingleThread = 0 # The max number of iterations # High values are basically unlimited (good for testing over night) # +# Note: This setting is not used in the Automatic Test Mode. There the test runs until every core has found a +# good value or has reached the "maxValue" setting +# # Default: 10000 maxIterations = 10000 @@ -572,11 +575,32 @@ enableAutomaticAdjustment = 0 startValues = CurrentValues +# Curve Optimizer / voltage offset values that you already know to be stable +# Cores listed here are treated as already finished: they will never be tested, not even after +# an automatic resume, and their value is included in the results and the final summary +# The format is :, separated by commas, spaces or the "|" character +# The enumeration of cores starts with 0 +# +# You can find a ready to use line at the end of the "_automode-results.txt" file of a previous run +# +# Note: This setting takes precedence over "startValues" for the listed cores +# Note: If a core is also listed in coresToIgnore, the coresToIgnore setting wins +# Note: This setting only works for Ryzen processors (Curve Optimizer), as the Intel +# implementation can currently only set a single voltage offset for all cores +# +# Example: knownGoodValues = 3:-25, 7:-18 +# Default: (empty) +knownGoodValues = + + # The upper limit for the Curve Optimizer values / voltage offset # If this limit has been reached, no further adjustments will be performed -# Instead the core will now simply throw an error and the regular "skipCoreOnError" setting will be obeyed # This is either a Curve Optimizer value or a voltage offset value # +# Note: A core that still throws an error once it has reached this value will not be tested again, +# regardless of the "skipCoreOnError" setting. It will be listed as "could not be stabilized" +# in the final summary +# # IMPORTANT: Be sensible about this value, setting it too high into the positive could apply a too high # voltage to your CPU and may damage it! # @@ -595,16 +619,55 @@ maxValue = 0 incrementBy = Default +# The number of test runs without an error a core needs to be considered "good" +# A core counts as finished once it has completed this many test runs in a row without an error at the same +# Curve Optimizer / voltage offset value. If an error occurs, the value is made less aggressive and the counter +# starts over for this core +# Higher values give you more confidence, but the test will take proportionally longer +# +# Note: In the Automatic Test Mode, the "maxIterations" setting from the [General] section is not used +# The test keeps running until every core has either found a good value or has reached the "maxValue" setting +# +# Default: 3 +passesToConfirmCoreValue = 3 + + # Set only the currently tested core to the selected Curve Optimizer / voltage offset value -# All the other cores will be set to 0, resp. the determined maximum value if it's higher than 0 +# All the other cores will be set to 0, resp. the value from "voltageValueForNotTestedCores", +# or the determined maximum value if it's higher than any of those # This should prevent errors caused by other cores than the currently tested one, or at least diminish the chance for that # # Note: Currently this only has an effect for Ryzen processors, for Intel up to 14th gen there is only one voltage value +# Note: Cores for which a good value has already been found are also set to "voltageValueForNotTestedCores", +# unless you enable the "applyConfirmedValuesForNotTestedCores" setting below # # Default: 0 setVoltageOnlyForTestedCore = 0 +# If setVoltageOnlyForTestedCore above is enabled, you can define which Curve Optimizer / voltage offset value you want the other, +# currently not tested cores to be set to +# +# Note: If the "current value" for a core is higher than what is entered here, e.g. derived from the "startValues" setting or +# from errors during testing that caused an automatic adjustment, the higher value for this core will take priority over this setting +# to avoid instabilities +# +# Default: 0 +voltageValueForNotTestedCores = 0 + + +# Apply the already found good values to the cores that are not currently being tested +# This setting only has an effect if "setVoltageOnlyForTestedCore" is enabled +# By default all cores that are not currently being tested are set to "voltageValueForNotTestedCores", so that a +# crash can only have been caused by the tested core +# If you enable this setting, cores that already have a confirmed value will use that value instead. This tests the +# combination of all of the found values, but if the computer now crashes, it may have been caused by one of those +# other cores, and the result for the currently tested core may be wrong +# +# Default: 0 +applyConfirmedValuesForNotTestedCores = 0 + + # Repeat the test on a core if it has thrown an error and the Curve Optimizer / voltage offset value was increased # Setting this to 1 will restart the test, until it has not thrown an error, or until the maximum value has been reached # Setting it to 0, the script will continue to the next core in line as normal diff --git a/helpers/automode-startup-script.ps1 b/helpers/automode-startup-script.ps1 index 083f75a..fd9d59f 100644 --- a/helpers/automode-startup-script.ps1 +++ b/helpers/automode-startup-script.ps1 @@ -26,6 +26,7 @@ $taskPath = '\CoreCycler\' # This file is in \helpers, our main script is one level above $scriptRoot = Split-Path -Path $PSScriptRoot -Parent $autoModeFile = $scriptRoot + '\.automode' +$autoModeFileBak = $scriptRoot + '\.automode-bak' $maxTimeLimit = 12 * 60 * 60 @@ -219,58 +220,94 @@ try { - if (!(Test-Path -LiteralPath $autoModeFile -PathType Leaf)) { + if (!(Test-Path -LiteralPath $autoModeFile -PathType Leaf) -and !(Test-Path -LiteralPath $autoModeFileBak -PathType Leaf)) { Write-Text('The .automode file does not exist!') Write-Text('The file is needed to be able to continue the testing process, aborting.') Write-Text('(Looking for: ' + $autoModeFile + ')') Write-Text('') Remove-StartupTask - + throw [EndTryBlockException] 'The .automode file does not exist, aborting!' } # Try to get the info from the .automode file - Write-Text('Parsing the .automode file:') - Write-Text($autoModeFile) - Write-Text('') + # There are two generations of the file, so if the current one is broken or missing, we can fall back to the previous one + $autoModeInfoFromJson = $null + $lastErrorMessage = 'Could not parse the .automode file!' - $reader = [System.IO.File]::OpenText($autoModeFile) - $autoModeFileContentString = $reader.ReadToEnd().Trim() - $reader.Close() + foreach ($thisFilePath in @($autoModeFile, $autoModeFileBak)) { + if (!(Test-Path -LiteralPath $thisFilePath -PathType Leaf)) { + continue + } + Write-Text('Parsing the .automode file:') + Write-Text($thisFilePath) + Write-Text('') - try { - $autoModeInfoFromJson = ConvertFrom-Json $autoModeFileContentString - } - catch { - throw [AutoModeResumeFailedException] ('Possible file corruption detected, could not parse the .automode file!' + [Environment]::NewLine + 'Reason: ' + $_.Exception.Message) + # Everything that can fail for this generation of the file needs to happen inside this try block, + # otherwise we would never get to the backup generation + # This includes reading the file itself (e.g. a sharing violation) and all of the casts of the stored values + try { + $autoModeFileContentString = [System.IO.File]::ReadAllText($thisFilePath).Trim() + + $parsedJson = ConvertFrom-Json $autoModeFileContentString + + # We have some required properties + @('fileTimestamp', 'lastCoreTested', 'logFileCoreCycler', 'logFileStressTest', 'voltageValues') | ForEach-Object { + if (!($parsedJson -and ($parsedJson | Get-Member $_))) { + throw ('The .automode file is missing the entry "' + $_ + '"!') + } + } + + # There also needs to be a valid core that was being tested, otherwise we cannot resume anything + # CoreCycler would treat an invalid core number as a regular new run and start all over again + if ([Int] $parsedJson.lastCoreTested -lt 0) { + throw ('The .automode file doesn''t contain a valid tested core ("' + $parsedJson.lastCoreTested + '")!') + } + + $fileTimestamp = [UInt64] $parsedJson.fileTimestamp + $lastCoreTested = [Int] $parsedJson.lastCoreTested + $logFileCoreCycler = [String] $parsedJson.logFileCoreCycler + $logFileStressTest = [String] $parsedJson.logFileStressTest + $voltageValues = [Array] $parsedJson.voltageValues + $waitBeforeResume = $(if ($parsedJson | Get-Member 'waitBeforeResume') { [Int] $parsedJson.waitBeforeResume } else { 0 }) # Optional + + # Optional entries of the newer file format + $schemaVersion = $(if ($parsedJson | Get-Member 'schemaVersion') { [Int] $parsedJson.schemaVersion } else { 1 }) + $storedIteration = $(if ($parsedJson | Get-Member 'iteration') { [Int] $parsedJson.iteration } else { 0 }) + $remainingCores = $(if ($parsedJson | Get-Member 'remainingCoreOrder') { @([Array] $parsedJson.remainingCoreOrder) } else { @() }) + + $autoModeInfoFromJson = $parsedJson + break + } + catch { + $lastErrorMessage = $_.Exception.Message + Write-Text('Could not use this file: ' + $lastErrorMessage) + Write-Text('') + } } - - # We have some required properties - @('fileTimestamp', 'lastCoreTested', 'logFileCoreCycler', 'logFileStressTest', 'voltageValues') | ForEach-Object { - if (!($autoModeInfoFromJson -and ($autoModeInfoFromJson | Get-Member $_))) { - throw [AutoModeResumeFailedException] ('The .automode file is missing the entry "' + $_ + '"!') - } + if (!$autoModeInfoFromJson) { + throw [AutoModeResumeFailedException] ('Possible file corruption detected, could not parse the .automode file!' + [Environment]::NewLine + 'Reason: ' + $lastErrorMessage) } - $fileTimestamp = [UInt64] $autoModeInfoFromJson.fileTimestamp - $lastCoreTested = [Int] $autoModeInfoFromJson.lastCoreTested - $logFileCoreCycler = [String] $autoModeInfoFromJson.logFileCoreCycler - $logFileStressTest = [String] $autoModeInfoFromJson.logFileStressTest - $voltageValues = [Array] $autoModeInfoFromJson.voltageValues - $waitBeforeResume = $(if ($autoModeInfoFromJson -and ($autoModeInfoFromJson | Get-Member 'waitBeforeResume')) { [Int] $autoModeInfoFromJson.waitBeforeResume } else { 0 }) # Optional - Write-Text('Timestamp: ' + $fileTimestamp) Write-Text('Tested Core: ' + $lastCoreTested) Write-Text('Logfile CoreCycler: ' + $logFileCoreCycler) Write-Text('Logfile Stress Test: ' + $logFileStressTest) Write-Text('Voltage Settings: ' + $voltageValues) Write-Text('Wait before resume: ' + $waitBeforeResume) + Write-Text('File version: ' + $schemaVersion) + + if ($schemaVersion -ge 2) { + Write-Text('Iteration: ' + $storedIteration) + Write-Text('Remaining cores: ' + $(if ($remainingCores.Count -gt 0) { $remainingCores -Join ', ' } else { '(none)' })) + } + Write-Text('') @@ -296,7 +333,7 @@ try { if ($fileTimestamp -lt $limitTimestamp) { $actualTimeDiff = $curTimeStamp - $fileTimestamp - throw [AutoModeResumeFailedException] ('The resume timestamp is too long ago (too much time has passed: ' + [Math]::Round($actualTimeDiff / 60 / 60, 1) + ' hours, max: ' + [Math]::Round($limitTime / 60 / 60, 1) + ' hours)') + throw [AutoModeResumeFailedException] ('The resume timestamp is too long ago (too much time has passed: ' + [Math]::Round($actualTimeDiff / 60 / 60, 1) + ' hours, max: ' + [Math]::Round($maxTimeLimit / 60 / 60, 1) + ' hours)') } diff --git a/readme.txt b/readme.txt index a02eb7c..1fdf289 100644 --- a/readme.txt +++ b/readme.txt @@ -69,6 +69,78 @@ setting in the config.ini to enable testing with two threads as well. +AUTOMATIC TEST MODE +------------------- +Instead of adjusting the Curve Optimizer values by hand after every error, you can let CoreCycler do it for you. +Enable it with "enableAutomaticAdjustment = 1" in the [AutomaticTestMode] section of your config file, or use one of +the ready made config files in the /configs/ directory (Ryzen.AutomaticTestMode.Start.ini resp. +Intel.AutomaticTestMode.yCruncher.ini). +Note that this requires administrator privileges, and for Ryzen processors it also requires PawnIO to be installed +(https://pawnio.eu/), as it uses the included "ryzen-smu-cli" to apply the Curve Optimizer values. For Intel it uses +the included "IntelVoltageControl", which sets a single voltage offset for all of the cores. + +How it works: +- It starts with the values from the "startValues" setting. For Ryzen you can use "Minimum" here, which selects the + lowest possible Curve Optimizer value of your processor (-30 for Ryzen 5000, -50 for Ryzen 7000 and upwards). +- Whenever a core throws an error, its value is made less aggressive by the "incrementBy" amount, and the counter of + error free test runs for this core starts over. +- Once a core has completed "passesToConfirmCoreValue" test runs in a row without an error at the same value, this + value is considered good. The core is then not tested again, not even after an automatic resume. +- If a core still throws an error once it has reached the "maxValue" setting, no stable value could be found for it. + It will not be tested again either, and it will be listed as "could not be stabilized" in the summary. +- The test keeps running until every core has either found a good value or has reached the "maxValue" setting. + The "maxIterations" setting from the [General] section is NOT used in the Automatic Test Mode. + +The results: +Every found value is immediately written to a "*_automode-results.txt" file in the /logs/ directory, next to the +regular log file. This file is never overwritten and also survives a crash or a reboot, so you always have the +values that were found up to that point. +At the end of the file (and in the summary at the end of a run) you will also find a ready to use line like this: + knownGoodValues = 0:-22, 1:-19, 2:-25, ... +You can copy this into the [AutomaticTestMode] section of your config file. The cores listed there will then be +treated as already finished and will not be tested again, which is handy if you want to continue a test run later on +or if you want to test only the remaining cores. + + NOTE: The "knownGoodValues" setting only works for Ryzen processors. For Intel there currently is only a single + voltage offset for all of the cores, so per core values make no sense, and CoreCycler will abort with an + error message if you use this setting on an Intel processor. + +IMPORTANT: CoreCycler does NOT store the Curve Optimizer / voltage offset values permanently! They are only applied + for the current Windows session. To keep them, you need to set them in your BIOS, or apply them on every + Windows start with a tool such as PBO2 Tuner or with the included ryzen-smu-cli. + +Resuming after a crash: +An unstable Curve Optimizer setting will usually not produce a nice error message, it will crash or freeze the +computer instead. With "enableResumeAfterUnexpectedExit = 1", CoreCycler creates a Scheduled Task that tries to +continue the test process after the reboot. It will retest the core that was being tested when the crash happened +(with an adjusted value) and then continue with the cores that had not been tested yet in that iteration - it does +not start the whole test order from the beginning again. + + NOTE: The Scheduled Task is executed when you log on, so for a truly unattended test run you should enable + Windows Auto Logon. Otherwise the computer will just sit at the logon screen after a crash. + https://learn.microsoft.com/en-us/sysinternals/downloads/autologon + NOTE: On some systems, especially Ryzen 9000 systems, an unstable Curve Optimizer setting does not reboot the + computer, it just freezes it. In that case nothing can resume automatically and you have to restart the + computer yourself. All of the values that were found up to that point are still in the results file. + NOTE: If the computer keeps crashing right after a resume, so that not a single core can be tested anymore and no + value can be increased anymore, CoreCycler gives up after a couple of attempts, removes the Scheduled Task + and aborts, instead of letting the computer reboot over and over again. + NOTE: Always exit CoreCycler with CTRL+C. If you just close the window, the Scheduled Task will remain and will + be executed on the next reboot. + +Other notes: +- Because a crash cannot be attributed to a specific core with certainty, it is highly recommended to also enable + "setVoltageOnlyForTestedCore", so that all of the other cores run at a safe value while one core is being tested. + With this setting, cores that already have a good value are also set to the safe value - if you want to test the + combination of all of the found values instead, enable "applyConfirmedValuesForNotTestedCores". +- There is an internal safety limit for the number of test runs per core, which cannot be configured. If a core + exceeds it (which should never happen), it is treated as "could not be stabilized" and the run continues with the + remaining cores. +- Using the Automatic Test Mode with very aggressive starting values can corrupt your Windows installation, so + creating a System Restore Point beforehand is highly recommended ("createSystemRestorePoint", enabled by default). + + + INCLUDED SOFTWARE ----------------- The script itself is a PowerShell script, but it uses other, inlcuded software to do the actual stress testing, for diff --git a/script-corecycler.ps1 b/script-corecycler.ps1 index 3be3a3c..7f2d0f9 100644 --- a/script-corecycler.ps1 +++ b/script-corecycler.ps1 @@ -2,7 +2,7 @@ .AUTHOR sp00n .VERSION - 0.11.0.4 + 0.11.1.0 .DESCRIPTION Sets the affinity of the selected stress test program process to only one core and cycles through all the cores which allows to test the stability of @@ -23,7 +23,7 @@ param( # Our current version -$version = '0.11.0.4' +$version = '0.11.1.0' # This defines the strict mode @@ -180,10 +180,29 @@ $pboCliTool = $PSScriptRoot + '\tools\ryzen-smu-cli $intelCliTool = $PSScriptRoot + '\tools\IntelVoltageControl\IntelVoltageControl.exe' $autoModeFile = $PSScriptRoot + '\.automode' $autoModeFileTemp = $PSScriptRoot + '\.automode-temp' +$autoModeFileBak = $PSScriptRoot + '\.automode-bak' $autoModeStartupScriptFile = $PSScriptRoot + '\helpers\automode-startup-script.ps1' $autoModeTaskName = 'CoreCycler AutoMode Startup Task' $autoModeTaskPath = '\CoreCycler\' $autoModeTaskDescription = 'CoreCycler Automatic Test Mode Startup Script' +$autoModeSchemaVersion = 2 +$autoModeResultsFileName = '' +$autoModeResultsFileFullPath = '' +$autoModeResultsFooterWritten = $false +$coreStates = @{} +$passesToConfirmCoreValue = 3 +$maxTestsPerCoreSanityLimit = 0 +$knownGoodValues = @{} +$applyConfirmedValuesForNotTestedCores = $false +$useResumedCoreOrder = $false +$resumedCoreOrder = @() +$resumedIteration = 1 +$resumedCoreStates = $null +$autoModeCurrentIteration = 1 +$autoModeRemainingCoreOrder = @() +$autoModeCurrentTestedCore = $CoreFromAutoMode # -1 if no core is being tested yet, otherwise the core from the crashed run +$autoModeResumeAttempts = 0 # The number of resume attempts in a row that couldn't test a single core nor increase a single value +$maxResumeAttemptsWithoutProgress = 10 # If this many resume attempts in a row have made no progress at all, we stop trying to resume # Parameters that are controllable by debug settings @@ -361,6 +380,9 @@ assignBothVirtualCoresForSingleThread = 0 # The max number of iterations # High values are basically unlimited (good for testing over night) # +# Note: This setting is not used in the Automatic Test Mode. There the test runs until every core has found a +# good value or has reached the "maxValue" setting +# # Default: 10000 maxIterations = 10000 @@ -785,11 +807,32 @@ enableAutomaticAdjustment = 0 startValues = CurrentValues +# Curve Optimizer / voltage offset values that you already know to be stable +# Cores listed here are treated as already finished: they will never be tested, not even after +# an automatic resume, and their value is included in the results and the final summary +# The format is :, separated by commas, spaces or the "|" character +# The enumeration of cores starts with 0 +# +# You can find a ready to use line at the end of the "_automode-results.txt" file of a previous run +# +# Note: This setting takes precedence over "startValues" for the listed cores +# Note: If a core is also listed in coresToIgnore, the coresToIgnore setting wins +# Note: This setting only works for Ryzen processors (Curve Optimizer), as the Intel +# implementation can currently only set a single voltage offset for all cores +# +# Example: knownGoodValues = 3:-25, 7:-18 +# Default: (empty) +knownGoodValues = + + # The upper limit for the Curve Optimizer values / voltage offset # If this limit has been reached, no further adjustments will be performed -# Instead the core will now simply throw an error and the regular "skipCoreOnError" setting will be obeyed # This is either a Curve Optimizer value or a voltage offset value # +# Note: A core that still throws an error once it has reached this value will not be tested again, +# regardless of the "skipCoreOnError" setting. It will be listed as "could not be stabilized" +# in the final summary +# # IMPORTANT: Be sensible about this value, setting it too high into the positive could apply a too high # voltage to your CPU and may damage it! # @@ -808,12 +851,27 @@ maxValue = 0 incrementBy = Default +# The number of test runs without an error a core needs to be considered "good" +# A core counts as finished once it has completed this many test runs in a row without an error at the same +# Curve Optimizer / voltage offset value. If an error occurs, the value is made less aggressive and the counter +# starts over for this core +# Higher values give you more confidence, but the test will take proportionally longer +# +# Note: In the Automatic Test Mode, the "maxIterations" setting from the [General] section is not used +# The test keeps running until every core has either found a good value or has reached the "maxValue" setting +# +# Default: 3 +passesToConfirmCoreValue = 3 + + # Set only the currently tested core to the selected Curve Optimizer / voltage offset value # All the other cores will be set to 0, resp. the value from "voltageValueForNotTestedCores", # or the determined maximum value if it's higher than any of those # This should prevent errors caused by other cores than the currently tested one, or at least diminish the chance for that # # Note: Currently this only has an effect for Ryzen processors, for Intel up to 14th gen there is only one voltage value +# Note: Cores for which a good value has already been found are also set to "voltageValueForNotTestedCores", +# unless you enable the "applyConfirmedValuesForNotTestedCores" setting below # # Default: 0 setVoltageOnlyForTestedCore = 0 @@ -830,6 +888,18 @@ setVoltageOnlyForTestedCore = 0 voltageValueForNotTestedCores = 0 +# Apply the already found good values to the cores that are not currently being tested +# This setting only has an effect if "setVoltageOnlyForTestedCore" is enabled +# By default all cores that are not currently being tested are set to "voltageValueForNotTestedCores", so that a +# crash can only have been caused by the tested core +# If you enable this setting, cores that already have a confirmed value will use that value instead. This tests the +# combination of all of the found values, but if the computer now crashes, it may have been caused by one of those +# other cores, and the result for the currently tested core may be wrong +# +# Default: 0 +applyConfirmedValuesForNotTestedCores = 0 + + # Repeat the test on a core if it has thrown an error and the Curve Optimizer / voltage offset value was increased # Setting this to 1 will restart the test, until it has not thrown an error, or until the maximum value has been reached # Setting it to 0, the script will continue to the next core in line as normal @@ -2938,6 +3008,11 @@ function Show-FinalSummary { $runTimeString = $runtimeArray -Join ', ' + # Store the found Curve Optimizer / voltage offset values permanently, no matter how the script ended + Write-AutoModeResultsFooter + + $autoModeSummaryLines = @(Get-AutoModeSummaryLines) + $testedCoresSorted = @($testedCoresArray.GetEnumerator() | Sort-Object -Property Name | ForEach-Object { 'Core ' + $_.Name.ToString() +' (' + $_.Value.ToString() + 'x)' }) $testedCoresGroups = [System.Collections.ArrayList]::new() $groupSize = 5 @@ -3082,6 +3157,16 @@ function Show-FinalSummary { } } + + # Display the Automatic Test Mode results + if ($autoModeSummaryLines.Count -gt 0) { + $returnString += [Environment]::NewLine + + foreach ($line in $autoModeSummaryLines) { + $returnString += ($line) + [Environment]::NewLine + } + } + return $returnString } @@ -3231,6 +3316,16 @@ function Show-FinalSummary { } + # Display the Automatic Test Mode results + if ($autoModeSummaryLines.Count -gt 0) { + Write-Text('') + + foreach ($line in $autoModeSummaryLines) { + Write-ColorText($line) Cyan + } + } + + # Display the log file location(s) Write-Text('') Write-ColorText('────────────────────────────────────────────────────────────────────────────────') Cyan @@ -3246,6 +3341,10 @@ function Show-FinalSummary { Write-ColorText((' - ' + $stressTestPrograms[$settings.General.stressTestProgram]['displayName'] + ':').PadRight($leftStringLength, ' ') + $stressTestLogFileName) Cyan } + if ($useAutomaticTestMode -and $autoModeResultsFileName) { + Write-ColorText((' - Auto Mode:').PadRight($leftStringLength, ' ') + $autoModeResultsFileName) Cyan + } + Write-Text('') } @@ -4344,18 +4443,22 @@ function Get-InitialUpdateCheckSetting { <# .DESCRIPTION - Parse the .automode file + Parse one specific .automode file (either the current or the backup generation) +.PARAMETER filePath + [String] The path to the file to parse .OUTPUTS - [HashTable] The parsed content from the .automode file + [HashTable] The parsed content from the provided file #> -function Get-AutoModeFileContent { - Write-DebugText('Parsing the .automode file') +function Get-ParsedAutoModeFile { + param( + [Parameter(Mandatory=$true)] [String] $filePath + ) - if (!(Test-Path -LiteralPath $autoModeFile -PathType Leaf)) { - throw('Could not find the .automode file!') + if (!(Test-Path -LiteralPath $filePath -PathType Leaf)) { + throw('Could not find the file "' + $filePath + '"!') } - $reader = [System.IO.File]::OpenText($autoModeFile) + $reader = [System.IO.File]::OpenText($filePath) $autoModeFileContentString = $reader.ReadToEnd().Trim() $reader.Close() @@ -4369,6 +4472,7 @@ function Get-AutoModeFileContent { # We have some required properties + # These are the ones that have existed since the very first version of the file, so we always require them @('fileTimestamp', 'lastCoreTested', 'logFileCoreCycler', 'logFileStressTest', 'voltageValues', 'waitBeforeResume') | ForEach-Object { if (!($autoModeInfoFromJson -and ($autoModeInfoFromJson | Get-Member $_))) { throw('The .automode file is missing the entry "' + $_ + '"!') @@ -4378,16 +4482,69 @@ function Get-AutoModeFileContent { # ConvertFrom-Json creates a PSCustomObject, which is hard to iterate, so create a hashtable instead $autoModeInfo = @{ - 'fileTimestamp' = [UInt64] $autoModeInfoFromJson.fileTimestamp - 'lastCoreTested' = [Int] $autoModeInfoFromJson.lastCoreTested - 'logFileCoreCycler' = [String] $autoModeInfoFromJson.logFileCoreCycler - 'logFileStressTest' = [String] $autoModeInfoFromJson.logFileStressTest - 'voltageValues' = [Array] $autoModeInfoFromJson.voltageValues - 'waitBeforeResume' = [Int] $autoModeInfoFromJson.waitBeforeResume + 'fileTimestamp' = [UInt64] $autoModeInfoFromJson.fileTimestamp + 'lastCoreTested' = [Int] $autoModeInfoFromJson.lastCoreTested + 'logFileCoreCycler' = [String] $autoModeInfoFromJson.logFileCoreCycler + 'logFileStressTest' = [String] $autoModeInfoFromJson.logFileStressTest + 'voltageValues' = [Array] $autoModeInfoFromJson.voltageValues + 'waitBeforeResume' = [Int] $autoModeInfoFromJson.waitBeforeResume + 'schemaVersion' = 1 + 'resultsFile' = '' + 'iteration' = 1 + 'resumeAttempts' = 0 + 'remainingCoreOrder' = @() + 'coreStates' = $null + } + + + # The additional entries of the version 2 schema + # These are all optional, an old file will simply fall back to the previous behavior + if ($autoModeInfoFromJson | Get-Member 'schemaVersion') { + $autoModeInfo['schemaVersion'] = [Int] $autoModeInfoFromJson.schemaVersion + } + + if ($autoModeInfo['schemaVersion'] -lt 2) { + Write-DebugText('The .automode file uses the old schema version ' + $autoModeInfo['schemaVersion']) + return $autoModeInfo + } + + if ($autoModeInfoFromJson | Get-Member 'resultsFile') { + $autoModeInfo['resultsFile'] = [String] $autoModeInfoFromJson.resultsFile + } + + if ($autoModeInfoFromJson | Get-Member 'iteration') { + $autoModeInfo['iteration'] = [Math]::Max(1, [Int] $autoModeInfoFromJson.iteration) + } + + if ($autoModeInfoFromJson | Get-Member 'resumeAttempts') { + $autoModeInfo['resumeAttempts'] = [Math]::Max(0, [Int] $autoModeInfoFromJson.resumeAttempts) + } + + if ($autoModeInfoFromJson | Get-Member 'remainingCoreOrder') { + $autoModeInfo['remainingCoreOrder'] = @(@([Array] $autoModeInfoFromJson.remainingCoreOrder) | ForEach-Object { [Int] $_ }) } - if ($autoModeInfo.lastCoreTested -ne $CoreFromAutoMode) { - throw('The passed core does not match the core in the .automode file! (' + $autoModeInfo.lastCoreTested + ' vs. ' + $CoreFromAutoMode + ')') + # The JSON keys are strings, we want an integer indexed hashtable + if ($autoModeInfoFromJson | Get-Member 'coreStates') { + $parsedCoreStates = @{} + + $autoModeInfoFromJson.coreStates.PSObject.Properties | ForEach-Object { + $thisCoreNumber = [Int] $_.Name + $thisCoreEntry = $_.Value + + $parsedCoreStates[$thisCoreNumber] = @{ + 'status' = [String] $thisCoreEntry.status + 'value' = [Int] $thisCoreEntry.value + 'passes' = [Int] $thisCoreEntry.passes + 'errors' = [Int] $thisCoreEntry.errors + 'tests' = [Int] $thisCoreEntry.tests + 'source' = [String] $thisCoreEntry.source + 'reason' = [String] $thisCoreEntry.reason + 'updatedAt' = [UInt64] $thisCoreEntry.updatedAt + } + } + + $autoModeInfo['coreStates'] = $parsedCoreStates } return $autoModeInfo @@ -4395,41 +4552,130 @@ function Get-AutoModeFileContent { +<# +.DESCRIPTION + Parse the .automode file + If the current generation of the file is missing or broken, the backup generation is used +.OUTPUTS + [HashTable] The parsed content from the .automode file +#> +function Get-AutoModeFileContent { + Write-DebugText('Parsing the .automode file') + + # We want to report the problem with the main file, not the one with the backup generation + $firstErrorMessage = $null + + foreach ($thisFilePath in @($autoModeFile, $autoModeFileBak)) { + try { + $autoModeInfo = Get-ParsedAutoModeFile -filePath $thisFilePath + + if ($autoModeInfo['lastCoreTested'] -ne $CoreFromAutoMode) { + throw('The passed core does not match the core in the .automode file! (' + $autoModeInfo['lastCoreTested'] + ' vs. ' + $CoreFromAutoMode + ')') + } + + if ($thisFilePath -ne $autoModeFile) { + Write-DebugText('Using the backup generation of the .automode file') + } + + return $autoModeInfo + } + catch { + Write-DebugText('Could not use "' + $thisFilePath + '": ' + $_.ToString()) + + if ($null -eq $firstErrorMessage) { + $firstErrorMessage = $_.ToString() + } + } + } + + throw($(if ($firstErrorMessage) { $firstErrorMessage } else { 'Could not find the .automode file!' })) +} + + + <# .DESCRIPTION Set/create the the .automode file + This keeps two generations of the file around (.automode and .automode-bak), so that there is always at least + one valid file on the disk, even if the computer crashes in the middle of the write process .PARAMETER coreNumber - [Int] The currently started core + [Int] (optional) The core that is now being tested, only provide this before its value is applied to the CPU +.PARAMETER iterationNumber + [Int] (optional) The current iteration +.PARAMETER remainingCoreOrder + [Array] (optional) The cores that still need to be tested in the current iteration, in their test order .OUTPUTS [Void] #> -function Set-AutoModeFile { +function Save-AutoModeState { param( - [Parameter(Mandatory=$true)] [Int] $coreNumber + [Parameter(Mandatory=$false)] $coreNumber, + [Parameter(Mandatory=$false)] $iterationNumber, + [Parameter(Mandatory=$false)] $remainingCoreOrder ) + # Only relevant if we want to be able to resume after a crash + if (!$useAutomaticTestModeWithResume) { + return + } + + # The currently tested core, the iteration and the remaining test order are only provided when a new core is being started + # All the other calls need to keep the last known values, otherwise we would lose them + # Especially the tested core must not be overwritten by a state change of some other core, as the crash after an + # unexpected exit is being attributed to it + # A core is being started, so the test process is making progress again and the resume attempts start over + if ($null -ne $coreNumber) { + $Script:autoModeCurrentTestedCore = [Int] $coreNumber + $Script:autoModeResumeAttempts = 0 + } + + if ($null -ne $iterationNumber) { + $Script:autoModeCurrentIteration = [Int] $iterationNumber + } + + if ($null -ne $remainingCoreOrder) { + $Script:autoModeRemainingCoreOrder = @($remainingCoreOrder) + } + + # No core is being tested yet, so there is nothing to resume to + # A file with an invalid core number would make the helper script re-start CoreCycler with this invalid core, + # which in turn would be treated as a completely new run, discarding all of the stored states + if ($autoModeCurrentTestedCore -lt 0) { + Write-DebugText('No core is being tested yet, not creating the .automode file') + return + } + Write-DebugText('Creating the .automode file') [UInt64] $curTimeStamp = Get-Date -UFormat %s -Millisecond 0 - # Remove the old file - if (Test-Path -LiteralPath $autoModeFile -PathType Leaf) { - $null = Remove-Item -LiteralPath $autoModeFile -Force + # The core states, with the core number as the key + $coreStatesForFile = @{} + + foreach ($entry in $coreStates.GetEnumerator()) { + $coreStatesForFile[$entry.Name.ToString()] = $entry.Value } $autoModeFileObject = @{ - 'fileTimestamp' = $curTimeStamp - 'lastCoreTested' = $coreNumber - 'logFileCoreCycler' = $logFileFullPath - 'logFileStressTest' = $stressTestLogFilePath - 'voltageValues' = $voltageCurrentValues - 'waitBeforeResume' = $settings['AutomaticTestMode']['waitBeforeAutomaticResume'] + 'schemaVersion' = $autoModeSchemaVersion + 'fileTimestamp' = $curTimeStamp + 'lastCoreTested' = $autoModeCurrentTestedCore + 'logFileCoreCycler' = $logFileFullPath + 'logFileStressTest' = $stressTestLogFilePath + 'voltageValues' = @($voltageCurrentValues) + 'waitBeforeResume' = $settings['AutomaticTestMode']['waitBeforeAutomaticResume'] + 'resultsFile' = $autoModeResultsFileFullPath + 'iteration' = $autoModeCurrentIteration + 'resumeAttempts' = $autoModeResumeAttempts + 'remainingCoreOrder' = @($autoModeRemainingCoreOrder) + 'coreStates' = $coreStatesForFile } # Convert to JSON - $autoModeFileJson = ConvertTo-Json $autoModeFileObject + # We need a higher depth than the default of 2 here, otherwise the core states would be mangled + $autoModeFileJson = ConvertTo-Json $autoModeFileObject -Depth 5 # We save the file under a different name, and then rename it, which will hopefully trigger the file content flush to disk @@ -4442,34 +4688,818 @@ function Set-AutoModeFile { [System.IO.File]::WriteAllLines($autoModeFileTemp, $autoModeFileJson) + # Verify that we can actually parse what we've just written + # If we cannot, we keep the previous generation of the file instead of replacing it with a broken one + try { + $null = ConvertFrom-Json ([System.IO.File]::ReadAllText($autoModeFileTemp).Trim()) + } + catch { + Write-VerboseText('Could not verify the newly written .automode-temp file, keeping the previous one!') + Write-VerboseText($_) + return + } + + # Try to flush the cache to the disk, hopefully reducing the amount of corrupted files if ($canUseFlushToDisk) { Save-CachedDataToDisk } - # Now rename the file - $null = Rename-Item -LiteralPath $autoModeFileTemp -NewName $autoModeFile -Force + # Keep the previous generation of the file around, it is only replaced by the next successful save + # A hard reset can leave the current file present but not yet flushed to the disk, in which case we can still + # fall back to this one + if (Test-Path -LiteralPath $autoModeFile -PathType Leaf) { + $null = Move-Item -LiteralPath $autoModeFile -Destination $autoModeFileBak -Force + } + + # Now rename the file + $null = Move-Item -LiteralPath $autoModeFileTemp -Destination $autoModeFile -Force + + if (!(Test-Path -LiteralPath $autoModeFile -PathType Leaf)) { + Exit-WithFatalError -text 'Could not create the .automode file!' + } +} + + + +<# +.DESCRIPTION + Set/create the the .automode file + Kept for compatibility, this is just a wrapper around Save-AutoModeState +.PARAMETER coreNumber + [Int] The currently started core +.OUTPUTS + [Void] +#> +function Set-AutoModeFile { + param( + [Parameter(Mandatory=$true)] [Int] $coreNumber + ) + + Save-AutoModeState -coreNumber $coreNumber +} + + + +<# +.DESCRIPTION + Remove the .automode file (and its temporary and backup generations) +.OUTPUTS + [Void] +#> +function Remove-AutoModeFile { + Write-DebugText('Removing the .automode file') + + foreach ($thisFilePath in @($autoModeFile, $autoModeFileBak, $autoModeFileTemp)) { + if (Test-Path -LiteralPath $thisFilePath -PathType Leaf) { + Remove-Item -LiteralPath $thisFilePath -Force -ErrorAction Ignore + } + } +} + + + +<# +.DESCRIPTION + Check if a core has a final Curve Optimizer / voltage offset value and doesn't need to be tested anymore +.PARAMETER coreNumber + [Int] The core to check +.OUTPUTS + [Bool] True if this core doesn't need to be tested anymore +#> +function Test-CoreIsResolved { + param( + [Parameter(Mandatory=$true)] [Int] $coreNumber + ) + + if (!$coreStates.ContainsKey($coreNumber)) { + return $false + } + + return (@('confirmed', 'unstable', 'ignored') -contains $coreStates[$coreNumber]['status']) +} + + + +<# +.DESCRIPTION + Get the Curve Optimizer / voltage offset values that the user has provided as already known to be good + Also validates the entries +.OUTPUTS + [HashTable] The known good values, with the core number as the key +#> +function Get-KnownGoodValuesFromSettings { + $foundValues = @{} + $entries = @($settings['AutomaticTestMode']['knownGoodValues']) + + if ($entries.Count -eq 0 -or [String]::IsNullOrWhiteSpace(($entries -Join ''))) { + return $foundValues + } + + + # For Intel we can currently only set a single voltage offset for all of the cores, so per-core values make no sense + if ($isIntelProcessor) { + Exit-WithFatalError -text ('The "knownGoodValues" setting is not supported on an Intel processor!' + [Environment]::NewLine + 'There currently is only a single voltage offset for all of the cores.') + } + + + $minCoValue = $(if ($processor.Name -Match '[7-9]\d{3}') { -50 } else { -30 } ) + $maxValue = [Int] $settings['AutomaticTestMode']['maxValue'] + + foreach ($entry in $entries) { + $thisEntry = ([String] $entry).Trim(' ', '"', '''', [Char]0x09) + + if ($thisEntry.Length -eq 0) { + continue + } + + # Reset the matches, otherwise we may get a stale match here (see #157) + $Matches = $null + + if ($thisEntry -notmatch '^(?\d+)\s*:\s*(?[+-]?\d+)$') { + $msg = 'Invalid "knownGoodValues" entry detected: "' + $thisEntry + '"' + $msg += [Environment]::NewLine + 'The expected format is :, e.g. knownGoodValues = 3:-25, 7:-18' + + Exit-WithFatalError -text $msg + } + + $thisCore = [Int] $Matches['core'] + $thisValue = [Int] $Matches['value'] + + if ($foundValues.ContainsKey($thisCore)) { + Exit-WithFatalError -text ('Core ' + $thisCore + ' is listed more than once in the "knownGoodValues" setting!') + } + + if ($thisCore -gt ($numPhysCores-1)) { + Exit-WithFatalError -text ('The "knownGoodValues" entry for core ' + $thisCore + ' is invalid, this CPU only has ' + $numPhysCores + ' physical cores (max is ' + ($numPhysCores-1) + ')!') + } + + if ([Math]::Abs($thisValue) -gt $limitForCoValues) { + Exit-WithFatalError -text ('Found an invalid value in the "knownGoodValues" setting (either higher or lower than +-' + $limitForCoValues + '): ' + $thisEntry) + } + + if ($thisValue -lt $minCoValue) { + Write-ColorText('Notice: The "knownGoodValues" entry for core ' + $thisCore + ' (' + $thisValue + ') is below the minimum value of this processor (' + $minCoValue + ')') Yellow + } + + if ($thisValue -gt $maxValue) { + Write-ColorText('Notice: The "knownGoodValues" entry for core ' + $thisCore + ' (' + $thisValue + ') is above the "maxValue" setting (' + $maxValue + '), using it anyway') Yellow + } + + if ($settings.General.coresToIgnore -contains $thisCore) { + Write-ColorText('Notice: Core ' + $thisCore + ' is listed in "knownGoodValues", but also in "coresToIgnore"') Yellow + Write-ColorText(' The "coresToIgnore" setting takes precedence, this core will not be touched at all') Yellow + continue + } + + $foundValues[$thisCore] = $thisValue + } + + return $foundValues +} + + + +<# +.DESCRIPTION + Build the state entries for all of the cores for the Automatic Test Mode + This is the authoritative source for which core still needs to be tested +.OUTPUTS + [Void] Sets $Script:coreStates +#> +function Initialize-CoreStates { + Write-DebugText('Initializing the core states') + + [UInt64] $curTimeStamp = Get-Date -UFormat %s -Millisecond 0 + + $newCoreStates = @{} + + for ($coreNumber = 0; $coreNumber -lt $numPhysCores; $coreNumber++) { + $newCoreStates[$coreNumber] = @{ + 'status' = 'pending' + 'value' = [Int] $voltageCurrentValues[$coreNumber] + 'passes' = 0 + 'errors' = 0 + 'tests' = 0 + 'source' = 'test' + 'reason' = '' + 'updatedAt' = $curTimeStamp + } + } + + + # Restore the states from the previous run if we're resuming after a crash + if ($resumedCoreStates) { + Write-VerboseText('Restoring the core states from the previous run') + + foreach ($entry in $resumedCoreStates.GetEnumerator()) { + $thisCoreNumber = [Int] $entry.Name + + if ($thisCoreNumber -lt 0 -or $thisCoreNumber -gt ($numPhysCores-1)) { + continue + } + + $newCoreStates[$thisCoreNumber] = $entry.Value + } + } + + + # Cores that are being ignored are never tested + foreach ($ignoredCore in @($settings.General.coresToIgnore)) { + $thisCoreNumber = [Int] $ignoredCore + + if ($thisCoreNumber -lt 0 -or $thisCoreNumber -gt ($numPhysCores-1)) { + continue + } + + $newCoreStates[$thisCoreNumber]['status'] = 'ignored' + $newCoreStates[$thisCoreNumber]['reason'] = '' + $newCoreStates[$thisCoreNumber]['updatedAt'] = $curTimeStamp + } + + + # The values that the user has provided as already known to be good + # These use the very same mechanism as a value that we have found ourselves, they're just flagged differently + $newCoresFromConfig = @() + + foreach ($entry in $knownGoodValues.GetEnumerator()) { + $thisCoreNumber = [Int] $entry.Name + $thisValue = [Int] $entry.Value + + if ($newCoreStates[$thisCoreNumber]['status'] -eq 'ignored') { + continue + } + + # Only announce a value that we didn't already know about from a previous run + $isNewEntry = !($newCoreStates[$thisCoreNumber]['status'] -eq 'confirmed' -and $newCoreStates[$thisCoreNumber]['source'] -eq 'config') + + $newCoreStates[$thisCoreNumber]['status'] = $(if ($isNewEntry) { 'pending' } else { 'confirmed' }) + $newCoreStates[$thisCoreNumber]['value'] = $thisValue + $newCoreStates[$thisCoreNumber]['passes'] = $passesToConfirmCoreValue + $newCoreStates[$thisCoreNumber]['source'] = 'config' + $newCoreStates[$thisCoreNumber]['reason'] = '' + $newCoreStates[$thisCoreNumber]['updatedAt'] = $curTimeStamp + + $Script:voltageStartingValues[$thisCoreNumber] = $thisValue + $Script:voltageCurrentValues[$thisCoreNumber] = $thisValue + + if ($isNewEntry) { + $newCoresFromConfig += $thisCoreNumber + } + + Write-VerboseText('Core ' + $thisCoreNumber + ' has a known good value of ' + $thisValue + ' from the config file, it will not be tested') + } + + + $Script:coreStates = $newCoreStates + + + # Store the values from the config file in the results file as well, so that the file is complete + foreach ($thisCoreNumber in $newCoresFromConfig) { + Set-CoreState -coreNumber $thisCoreNumber -status 'confirmed' -value $knownGoodValues[$thisCoreNumber] + } +} + + + +<# +.DESCRIPTION + Create the Automatic Test Mode results file, which permanently stores the found values + If we're resuming after a crash, the file of the previous run is re-used +.OUTPUTS + [Void] +#> +function Initialize-AutoModeResultsFile { + # We may already have gotten the path from the .automode file when resuming + if ([String]::IsNullOrWhiteSpace($autoModeResultsFileFullPath)) { + $Script:autoModeResultsFileName = ($logFileName -Replace '\.log$', '') + '_automode-results.txt' + $Script:autoModeResultsFileFullPath = $logFilePathAbsolute + $autoModeResultsFileName + } + else { + $Script:autoModeResultsFileName = Split-Path -LiteralPath $autoModeResultsFileFullPath -Leaf + } + + Write-DebugText('The Automatic Test Mode results file: ' + $autoModeResultsFileFullPath) + + # The file already exists, we're appending to it + if (Test-Path -LiteralPath $autoModeResultsFileFullPath -PathType Leaf) { + Write-AutoModeResultEntry -text '' -NoLogEntry + Write-AutoModeResultEntry -text ('Resumed: ' + (Get-Date -Format 'yyyy-MM-dd HH:mm:ss')) -NoLogEntry + return + } + + + $autoModeDescription = $(if ($isIntelProcessor) { 'voltage offset' } else { 'Curve Optimizer' }) + $settingsString = 'startValues = ' + ($settings['AutomaticTestMode']['startValues'] -Join ' ') + $settingsString += ' | maxValue = ' + $settings['AutomaticTestMode']['maxValue'] + $settingsString += ' | incrementBy = ' + $settings['AutomaticTestMode']['incrementBy'] + $settingsString += ' | passesToConfirmCoreValue = ' + $passesToConfirmCoreValue + + Write-AutoModeResultEntry -text ('CoreCycler Automatic Test Mode - ' + $autoModeDescription + ' results') -NoLogEntry + Write-AutoModeResultEntry -text ('Started: ' + (Get-Date -Format 'yyyy-MM-dd HH:mm:ss')) -NoLogEntry + Write-AutoModeResultEntry -text ('Version: ' + $version) -NoLogEntry + Write-AutoModeResultEntry -text ('Processor: ' + $processor.Name + ' (' + $numPhysCores + ' physical cores)') -NoLogEntry + Write-AutoModeResultEntry -text ('Settings: ' + $settingsString) -NoLogEntry + Write-AutoModeResultEntry -text ('────────────────────────────────────────────────────────────────────────────────') -NoLogEntry +} + + + +<# +.DESCRIPTION + Append a line to the Automatic Test Mode results file + The file is append-only, so a crash can at worst cut off the very last line +.PARAMETER text + [String] The line to append +.PARAMETER NoLogEntry + [Switch] (optional) If set, the line will not additionally be written to the regular log file +.OUTPUTS + [Void] +#> +function Write-AutoModeResultEntry { + param( + [Parameter(Mandatory=$true)][AllowEmptyString()] [String] $text, + [Parameter(Mandatory=$false)] [Switch] $NoLogEntry + ) + + if ([String]::IsNullOrWhiteSpace($autoModeResultsFileFullPath)) { + return + } + + for ($numTry = 1; $numTry -le 3; $numTry++ ) { + try { + # The second parameter defines if to append ($true) or overwrite ($false) + $stream = [System.IO.StreamWriter]::new($autoModeResultsFileFullPath, $true, ([System.Text.Utf8Encoding]::new())) + $stream.WriteLine($text) + $stream.Close() + + break + } + catch { + Write-DebugText('Couldn''t write the Automatic Test Mode results file on try ' + $numTry) + Start-Sleep -Milliseconds 250 + } + } + + if (-not $NoLogEntry.IsPresent) { + Write-LogEntry($text) + } +} + + + +<# +.DESCRIPTION + Try to re-build the resolved core states from an existing results file + This is the last resort if the .automode file cannot be used anymore +.PARAMETER filePath + [String] The path to the results file +.OUTPUTS + [HashTable] The core states that could be recovered, with the core number as the key +#> +function Read-AutoModeResultsFile { + param( + [Parameter(Mandatory=$true)] [String] $filePath + ) + + $recoveredStates = @{} + + if ([String]::IsNullOrWhiteSpace($filePath) -or !(Test-Path -LiteralPath $filePath -PathType Leaf)) { + return $recoveredStates + } + + Write-VerboseText('Trying to recover the already found values from the results file') + + [UInt64] $curTimeStamp = Get-Date -UFormat %s -Millisecond 0 + + foreach ($line in @([System.IO.File]::ReadAllLines($filePath))) { + # Reset the matches, otherwise we may get a stale match here (see #157) + $Matches = $null + + if ($line -notmatch '^\S+ \S+\s+Core\s+(\d+)\s+(KNOWN|CONFIRMED|UNSTABLE)\s+\S+\s+(-?\d+)') { + continue + } + + $thisCoreNumber = [Int] $Matches[1] + $thisEventType = [String] $Matches[2] + $thisValue = [Int] $Matches[3] + + $recoveredStates[$thisCoreNumber] = @{ + 'status' = $(if ($thisEventType -eq 'UNSTABLE') { 'unstable' } else { 'confirmed' }) + 'value' = $thisValue + 'passes' = $passesToConfirmCoreValue + 'errors' = 0 + 'tests' = 0 + 'source' = $(if ($thisEventType -eq 'KNOWN') { 'config' } else { 'test' }) + 'reason' = $(if ($thisEventType -eq 'UNSTABLE') { 'maxValue' } else { '' }) + 'updatedAt' = $curTimeStamp + } + } + + return $recoveredStates +} + + + +<# +.DESCRIPTION + Build the "knownGoodValues" string for all of the cores that have a confirmed value +.PARAMETER providedCoreStates + [HashTable] (optional) The core states to use, defaults to the current ones +.PARAMETER OnlyFromConfig + [Switch] (optional) If set, will only include the values that the user has provided in the config file +.OUTPUTS + [String] The string for the config file, or an empty string if there are no confirmed values +#> +function Get-ConfirmedValuesString { + param( + [Parameter(Mandatory=$false)] $providedCoreStates, + [Parameter(Mandatory=$false)] [Switch] $OnlyFromConfig + ) + + $statesToUse = $(if ($providedCoreStates) { $providedCoreStates } else { $coreStates }) + $entries = @() + + foreach ($entry in @($statesToUse.GetEnumerator() | Sort-Object -Property { [Int] $_.Name })) { + if ($entry.Value['status'] -ne 'confirmed') { + continue + } + + if ($OnlyFromConfig.IsPresent -and $entry.Value['source'] -ne 'config') { + continue + } + + $entries += ([Int] $entry.Name).ToString() + ':' + ([Int] $entry.Value['value']).ToString() + } + + if ($entries.Count -eq 0) { + return '' + } + + return ($entries -Join ', ') +} + + + +<# +.DESCRIPTION + If a leftover .automode file from a previous, crashed run contains already confirmed values, display them + before the file is being discarded, so that they're not silently lost +.OUTPUTS + [Void] +#> +function Show-DiscardedAutoModeResults { + if (!(Test-Path -LiteralPath $autoModeFile -PathType Leaf) -and !(Test-Path -LiteralPath $autoModeFileBak -PathType Leaf)) { + return + } + + foreach ($thisFilePath in @($autoModeFile, $autoModeFileBak)) { + try { + $previousInfo = Get-ParsedAutoModeFile -filePath $thisFilePath + } + catch { + continue + } + + if (!$previousInfo['coreStates']) { + continue + } + + $previousValuesString = Get-ConfirmedValuesString -providedCoreStates $previousInfo['coreStates'] + + if ([String]::IsNullOrWhiteSpace($previousValuesString)) { + return + } + + Write-Text('') + Write-ColorText('There is a leftover state file from a previous Automatic Test Mode run.') Yellow + Write-ColorText('It already contained values for the following cores:') Yellow + Write-ColorText('knownGoodValues = ' + $previousValuesString) Cyan + Write-ColorText('You can add this line to the [AutomaticTestMode] section of your config file if you') Yellow + Write-ColorText('do not want these cores to be tested again.') Yellow + Write-Text('') + + return + } +} + + + +<# +.DESCRIPTION + Set the state of a core and store it permanently + This is the only place where the status of a core is being changed +.PARAMETER coreNumber + [Int] The core to change +.PARAMETER status + [String] The new status (pending, testing, confirmed, unstable, ignored) +.PARAMETER value + [Int] (optional) The Curve Optimizer / voltage offset value for this core +.PARAMETER reason + [String] (optional) Why a core is now "unstable" (maxValue or sanityLimit) +.OUTPUTS + [Void] +#> +function Set-CoreState { + param( + [Parameter(Mandatory=$true)] [Int] $coreNumber, + [Parameter(Mandatory=$true)] [String] $status, + [Parameter(Mandatory=$false)] $value, + [Parameter(Mandatory=$false)] [String] $reason = '' + ) + + if (!$coreStates.ContainsKey($coreNumber)) { + return + } + + [UInt64] $curTimeStamp = Get-Date -UFormat %s -Millisecond 0 + + $thisCoreState = $coreStates[$coreNumber] + $previousStatus = $thisCoreState['status'] + $thisCoreState['status'] = $status + $thisCoreState['reason'] = $reason + $thisCoreState['updatedAt'] = $curTimeStamp + + if ($null -ne $value) { + $thisCoreState['value'] = [Int] $value + } + + + # Store the found values also in the "legacy" arrays, which are being used for the various summaries + if ($status -eq 'unstable') { + if ($coresWithErrorAndMaxVoltageValue -notcontains $coreNumber) { + [Void] $Script:coresWithErrorAndMaxVoltageValue.Add($coreNumber) + } + } + + + # Store the new state before the permanent entry is appended to the results file + # The two files cannot be written at the very same moment, and the results file is the more durable one (it is + # append-only), so it has to be the last step: a final value in the results file that the resumable state doesn't + # know about yet is recovered on the next start, while the other way around the core would be tested again + # No -coreNumber here, a state change must not change the core that is currently being tested + Save-AutoModeState + + + # Write the permanent entry for a final value + if ($previousStatus -ne $status -and @('confirmed', 'unstable') -contains $status) { + $autoModeDescription = $(if ($useCurveOptimizer) { 'Curve Optimizer' } else { 'voltage offset' }) + $valueLabel = $(if ($useCurveOptimizer) { 'CO' } else { 'mV' }) + $thisValue = [Int] $thisCoreState['value'] + $timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss' + $eventType = '' + $noteString = '' + + if ($status -eq 'confirmed') { + $eventType = $(if ($thisCoreState['source'] -eq 'config') { 'KNOWN' } else { 'CONFIRMED' }) + + if ($thisCoreState['source'] -eq 'config') { + $noteString = 'provided in the config file' + + if ($canUseWindowsEventLog) { + Write-AppEventLog -type 'core_co_known' -infoString1 $coreNumber -infoString2 $thisValue + } + } + else { + $noteString = $thisCoreState['passes'].ToString() + '/' + $passesToConfirmCoreValue + ' passes, ' + $thisCoreState['errors'] + ' errors' + + if ($canUseWindowsEventLog) { + Write-AppEventLog -type 'core_co_confirmed' -infoString1 $coreNumber -infoString2 $thisValue -infoString3 $noteString + } + + Write-Text('') + Write-ColorText('Core ' + $coreNumber + ' has completed ' + $passesToConfirmCoreValue + ' test runs without an error, the ' + $autoModeDescription + ' value of ' + $thisValue + ' is confirmed') Green + Write-Text('') + } + } + else { + $eventType = 'UNSTABLE' + $noteString = $(if ($reason -eq 'maxValue') { 'maximum value reached and still erroring' } else { 'internal safety limit reached' }) + + if ($canUseWindowsEventLog) { + Write-AppEventLog -type 'core_co_unstable' -infoString1 $coreNumber -infoString2 $thisValue -infoString3 $noteString + } + + Write-Text('') + Write-ColorText('No stable ' + $autoModeDescription + ' value could be found for core ' + $coreNumber + ' (' + $noteString + ')') Red + Write-ColorText('This core will not be tested again') Red + Write-Text('') + } + + $resultLine = $timestamp + ' Core ' + $coreNumber.ToString().PadLeft(3, ' ') + ' ' + $eventType.PadRight(9, ' ') + $resultLine += ' ' + $valueLabel + ' ' + $thisValue.ToString().PadLeft(5, ' ') + $resultLine += ' (' + $noteString + ')' + + Write-AutoModeResultEntry -text $resultLine -NoLogEntry + } +} + + + +<# +.DESCRIPTION + Register a test run that has completed without an error for a core + If enough consecutive passes have been collected, the value for this core is considered confirmed +.PARAMETER coreNumber + [Int] The core that has completed a test run +.OUTPUTS + [Void] +#> +function Add-CorePass { + param( + [Parameter(Mandatory=$true)] [Int] $coreNumber + ) + + if (!$useAutomaticTestMode -or !$coreStates.ContainsKey($coreNumber)) { + return + } + + # A core that already has a final value doesn't collect any more passes + if (Test-CoreIsResolved $coreNumber) { + return + } + + $thisCoreState = $coreStates[$coreNumber] + $thisCoreState['passes']++ + + $autoModeDescription = $(if ($useCurveOptimizer) { 'Curve Optimizer' } else { 'voltage offset' }) + $thisValue = $(if ($useCurveOptimizer) { [Int] $voltageCurrentValues[$coreNumber] } else { [Int] $voltageCurrentValues[0] }) + + if ($thisCoreState['passes'] -ge $passesToConfirmCoreValue) { + Set-CoreState -coreNumber $coreNumber -status 'confirmed' -value $thisValue + return + } + + Write-ColorText(' Core ' + $coreNumber + ': ' + $thisCoreState['passes'] + '/' + $passesToConfirmCoreValue + ' confirmation passes at a ' + $autoModeDescription + ' value of ' + $thisValue) Cyan + + # No -coreNumber here, a completed pass must not change the core that is currently being tested + Save-AutoModeState +} + + + +<# +.DESCRIPTION + Reset the number of collected error free test runs for a core, e.g. because it has thrown an error +.PARAMETER coreNumber + [Int] The core to reset +.PARAMETER CountError + [Switch] (optional) If set, will also increase the error counter for this core +.OUTPUTS + [Void] +#> +function Reset-CorePasses { + param( + [Parameter(Mandatory=$true)] [Int] $coreNumber, + [Parameter(Mandatory=$false)] [Switch] $CountError + ) + + if (!$coreStates.ContainsKey($coreNumber)) { + return + } + + $coreStates[$coreNumber]['passes'] = 0 + + if ($CountError.IsPresent) { + $coreStates[$coreNumber]['errors']++ + } +} + + + +<# +.DESCRIPTION + Build the Automatic Test Mode part of the final summary + Used for both the displayed and the returned version of the summary, as well as for the results file +.OUTPUTS + [Array] The lines for the summary +#> +function Get-AutoModeSummaryLines { + $lines = @() + + if (!$useAutomaticTestMode -or $coreStates.Count -lt 1) { + return $lines + } + + $autoModeDescription = $(if ($useCurveOptimizer) { 'Curve Optimizer' } else { 'voltage offset' }) + $numConfirmedCores = 0 + $numUnstableCores = 0 + $unstableCores = @() + $unresolvedCores = @() + $statusEntries = @() + + for ($coreNumber = 0; $coreNumber -lt $numPhysCores; $coreNumber++) { + $thisStatus = $(if ($coreStates.ContainsKey($coreNumber)) { $coreStates[$coreNumber]['status'] } else { 'pending' }) + $thisSource = $(if ($coreStates.ContainsKey($coreNumber)) { $coreStates[$coreNumber]['source'] } else { 'test' }) + $thisReason = $(if ($coreStates.ContainsKey($coreNumber)) { $coreStates[$coreNumber]['reason'] } else { '' }) + $thisLabel = '--' + + if ($thisStatus -eq 'confirmed') { + $thisLabel = $(if ($thisSource -eq 'config') { 'CFG' } else { 'OK' }) + $numConfirmedCores++ + } + elseif ($thisStatus -eq 'unstable') { + $thisLabel = $(if ($thisReason -eq 'sanityLimit') { 'LIM' } else { 'MAX' }) + $numUnstableCores++ + $unstableCores += $coreNumber.ToString() + } + elseif ($thisStatus -eq 'ignored') { + $thisLabel = 'IGN' + } + else { + $unresolvedCores += $coreNumber.ToString() + } + + $statusEntries += $thisLabel.PadLeft(4, ' ') + } + + + if ($useCurveOptimizer) { + # This block gets its own header row, so that the labels always line up with their core, + # no matter which of the tables above it is being displayed with + $coCoresString = ((0..($numPhysCores-1)) | ForEach-Object { ('C' + $_.ToString()).PadLeft(4, ' ') }) -Join ' |' + + $lines += ('Core ' + $coCoresString) + $lines += ('Status ' + ($statusEntries -Join ' |')) + $lines += '' + $lines += 'Legend: OK = confirmed by this run' + $lines += ' CFG = provided via knownGoodValues' + $lines += ' MAX = could not be stabilized, the maximum value was reached' + $lines += ' LIM = stopped by the internal safety limit' + $lines += ' IGN = ignored core' + $lines += ' -- = no final value (the run did not finish)' + $lines += '' + } + + $lines += ('Confirmed ' + $autoModeDescription + ' values for ' + $numConfirmedCores + ' of ' + $numPhysCores + ' cores') + + if ($numUnstableCores -gt 0) { + $lines += ($numUnstableCores.ToString() + ' core' + $(if ($numUnstableCores -gt 1) { 's' }) + ' could not be stabilized: ' + ($unstableCores -Join ', ')) + } + + $lines += ('Cores without a final value: ' + $(if ($unresolvedCores.Count -gt 0) { $unresolvedCores -Join ', ' } else { '(none)' })) + + + $confirmedValuesString = Get-ConfirmedValuesString - if (!(Test-Path -LiteralPath $autoModeFile -PathType Leaf)) { - Exit-WithFatalError -text 'Could not create the .automode file!' + if (![String]::IsNullOrWhiteSpace($confirmedValuesString) -and $useCurveOptimizer) { + $lines += '' + $lines += 'Copy this into your config file to keep the results:' + $lines += '[AutomaticTestMode]' + $lines += ('knownGoodValues = ' + $confirmedValuesString) + } + + $lines += '' + $lines += 'Note: CoreCycler does not store these values permanently. Set them in your BIOS' + $lines += ' (or with a tool such as PBO2 Tuner) to keep them after a reboot.' + + if (![String]::IsNullOrWhiteSpace($autoModeResultsFileFullPath)) { + $lines += '' + $lines += 'The Automatic Test Mode results have been written to:' + $lines += $autoModeResultsFileFullPath } + + return $lines } <# .DESCRIPTION - Remove the .automode file + Append the final block with all of the found values to the Automatic Test Mode results file + This runs on every exit path, so that even after CTRL+C or a fatal error there's a usable result + It is deliberately kept short, as this file is being appended to by every (resumed) run .OUTPUTS [Void] #> -function Remove-AutoModeFile { - Write-DebugText('Removing the .automode file') +function Write-AutoModeResultsFooter { + if (!$useAutomaticTestMode -or $autoModeResultsFooterWritten -or [String]::IsNullOrWhiteSpace($autoModeResultsFileFullPath)) { + return + } - if (Test-Path -LiteralPath $autoModeFile -PathType Leaf) { - Remove-Item -LiteralPath $autoModeFile + $Script:autoModeResultsFooterWritten = $true + + $autoModeDescription = $(if ($useCurveOptimizer) { 'Curve Optimizer' } else { 'voltage offset' }) + $numConfirmedCores = @(@($coreStates.GetEnumerator()) | Where-Object { $_.Value['status'] -eq 'confirmed' }).Count + $numUnstableCores = @(@($coreStates.GetEnumerator()) | Where-Object { $_.Value['status'] -eq 'unstable' }).Count + + $finishedLine = (Get-Date -Format 'yyyy-MM-dd HH:mm:ss') + ' RUN ENDED - ' + $numConfirmedCores + ' of ' + $numPhysCores + ' cores confirmed' + $finishedLine += $(if ($numUnstableCores -gt 0) { ', ' + $numUnstableCores + ' could not be stabilized' }) + + Write-AutoModeResultEntry -text ('────────────────────────────────────────────────────────────────────────────────') -NoLogEntry + Write-AutoModeResultEntry -text $finishedLine -NoLogEntry + + # Only the ready to use line for the config file, the whole summary is displayed on the screen and in the log file + $confirmedValuesString = Get-ConfirmedValuesString + + if (![String]::IsNullOrWhiteSpace($confirmedValuesString) -and $useCurveOptimizer) { + Write-AutoModeResultEntry -text '' -NoLogEntry + Write-AutoModeResultEntry -text ('The found ' + $autoModeDescription + ' values:') -NoLogEntry + Write-AutoModeResultEntry -text ('knownGoodValues = ' + $confirmedValuesString) -NoLogEntry } + + Write-AutoModeResultEntry -text '' -NoLogEntry } @@ -4659,6 +5689,7 @@ function Import-Settings { $settingsWithArrayOrString = @( #'coreTestOrder' # This is handled later 'startValues' + 'knownGoodValues' ) # Check if the file exists @@ -5402,6 +6433,35 @@ function Initialize-AutomaticTestMode { Write-DebugText('The Automatic Test Mode starting values from the .automode file:') Write-DebugText('We will increase this value because of the crash') Write-DebugText($voltageStartValuesArray) + + + # Continue where we left off before the crash (see #106) + # The older schema version doesn't have these entries, in which case we start the test order from the beginning again + if ($autoModeInfo['schemaVersion'] -ge 2) { + $Script:resumedCoreStates = $autoModeInfo['coreStates'] + $Script:resumedIteration = [Int] $autoModeInfo['iteration'] + $Script:resumedCoreOrder = @($autoModeInfo['remainingCoreOrder']) + $Script:useResumedCoreOrder = $true + $Script:autoModeResultsFileFullPath = [String] $autoModeInfo['resultsFile'] + + # Keep the restored values, so that a crash before the first core has been started doesn't lose them + $Script:autoModeCurrentIteration = $Script:resumedIteration + $Script:autoModeRemainingCoreOrder = @($Script:resumedCoreOrder) + + Write-DebugText('The iteration before the crash: ' + $resumedIteration) + Write-DebugText('The remaining test order before the crash: ' + ($resumedCoreOrder -Join ', ')) + } + else { + Write-VerboseText('The .automode file is from an older version, the test order will start from the beginning') + } + + + # Count this resume attempt + # The counter is set back to 0 as soon as a core is started or a value has actually been increased, so it + # only keeps rising if a resume cannot make any progress at all (see the check further down) + $Script:autoModeResumeAttempts = [Int] $autoModeInfo['resumeAttempts'] + 1 + + Write-DebugText('The number of resume attempts without any progress: ' + $autoModeResumeAttempts) } } @@ -5420,6 +6480,11 @@ function Initialize-AutomaticTestMode { $voltageStartValuesArray = @($voltageStartValuesString -Split '\s+') + # There may be a leftover state file from a previous, crashed run + # We're going to discard it, but we don't want to silently throw away the values that were already found + Show-DiscardedAutoModeResults + + # At this point, we also want to ask for the creation of a System Restore Point if ($settings['AutomaticTestMode']['createSystemRestorePoint'] -gt 0) { Test-CreateNewSystemRestorePoint @@ -5492,6 +6557,64 @@ function Initialize-AutomaticTestMode { $Script:voltageValueForNotTestedCores = $settings.AutomaticTestMode.voltageValueForNotTestedCores + # How many test runs in a row without an error a core needs to be considered "good" + $Script:passesToConfirmCoreValue = [Math]::Max(1, [Int] $settings.AutomaticTestMode.passesToConfirmCoreValue) + $Script:applyConfirmedValuesForNotTestedCores = ($settings.AutomaticTestMode.applyConfirmedValuesForNotTestedCores -gt 0) + + + # The values that the user has already found to be good, these cores will not be tested at all + $Script:knownGoodValues = Get-KnownGoodValuesFromSettings + + + # An internal safety limit for the number of test runs for a single core + # This should never be reached, it only exists so that a core cannot be tested indefinitely if something goes wrong + $incrementByValue = [Math]::Max(1, [Int] $settings.AutomaticTestMode.incrementBy) + $maxValueSetting = [Int] $settings.AutomaticTestMode.maxValue + $largestDistance = 0 + + $voltageStartValuesArray | ForEach-Object { + $largestDistance = [Math]::Max($largestDistance, [Math]::Abs([Int] $_ - $maxValueSetting)) + } + + $Script:maxTestsPerCoreSanityLimit = (([Math]::Ceiling($largestDistance / $incrementByValue) + 2) * $passesToConfirmCoreValue) + 10 + + Write-DebugText('The internal safety limit for the number of tests per core: ' + $maxTestsPerCoreSanityLimit) + + + # The permanent results file, which stores the found values even if the .automode file is gone + Initialize-AutoModeResultsFile + + # The results file belongs to this very run (its path travels in the .automode file), and it is written after the + # state has been stored, so it can hold a final value that the restored state doesn't know about yet + # If the .automode file didn't have the core states at all (e.g. because it was written by an older version), + # the results file is the only source for the already found values + # Without this a torn state save would make the script test an already confirmed core again (M1) + if ($CoreFromAutoMode -gt -1 -and ![String]::IsNullOrWhiteSpace($autoModeResultsFileFullPath)) { + $recoveredCoreStates = Read-AutoModeResultsFile -filePath $autoModeResultsFileFullPath + + if (!$resumedCoreStates) { + $Script:resumedCoreStates = $recoveredCoreStates + } + else { + foreach ($entry in $recoveredCoreStates.GetEnumerator()) { + $thisCoreNumber = [Int] $entry.Name + + # The restored state already has a final value for this core, and it holds more details (passes, errors, tests) + if ($resumedCoreStates.ContainsKey($thisCoreNumber) -and @('confirmed', 'unstable', 'ignored') -contains $resumedCoreStates[$thisCoreNumber]['status']) { + continue + } + + Write-VerboseText('Core ' + $thisCoreNumber + ' already has a final value in the results file, restoring it from there') + + $Script:resumedCoreStates[$thisCoreNumber] = $entry.Value + } + } + } + + # The state of each core, this decides which core still needs to be tested + Initialize-CoreStates + + if ($useAutomaticTestModeWithResume) { Write-VerboseText('Automatic Test Mode with resuming after unexpected exit enabled') @@ -5507,6 +6630,70 @@ function Initialize-AutomaticTestMode { } + # Adjust the value of the previously tested core from before the reboot if we're in Automatic Test Mode with resume + # This has to happen before any value is applied to the processor, otherwise we would set the very same value that + # has caused the crash again, which can crash the machine once more before the increased value has been stored, + # repeating this forever (especially with setVoltageOnlyForTestedCore = 0, where all of the cores get their values here) + # The core itself is put back to the front of the test order via the persisted remaining test order (see #106) + if ($useAutomaticTestModeWithResume -and $CoreFromAutoMode -gt -1) { + $timestamp = Get-Date -Format HH:mm:ss + Write-DebugText($timestamp) + Write-Text('') + Write-ColorText('Apparently the computer crashed in the last run while testing core ' + $CoreFromAutoMode) Red + Write-ColorText('Trying to resume the test process') Red + + + # The last resume attempts couldn't test a single core anymore, nor could they increase a single value (e.g. + # because the crashed core has already reached the maximum value, in which case there is no safer value left + # that we would be allowed to apply) + # Applying the stored values once more would only lead to the next crash and the next reboot, so we stop here + # The Scheduled Task and the .automode file are removed by the finally block + if ($autoModeResumeAttempts -gt $maxResumeAttemptsWithoutProgress) { + Write-Text('') + Write-ColorText('The test process has now been resumed ' + $autoModeResumeAttempts + ' times in a row without being able to test a single core!') Red + Write-ColorText('The computer apparently already crashes with the stored ' + $modeDescription + ' values before the test can even start.') Red + Write-ColorText('Aborting, so that it doesn''t keep rebooting forever.') Red + Write-Text('') + Write-ColorText('You may want to select a less aggressive "maxValue" setting, or set a stable value for the') Yellow + Write-ColorText('affected core(s) in your BIOS.') Yellow + + Exit-Script -errorCode 7 + } + + + # The Windows Event Log Source is only added further down in the startup process, but the adjustment below + # can already produce Event Log entries (e.g. "core_co_unstable"), which would be skipped without it + if ($settings.Logging.useWindowsEventLog -and (Test-EventLogService)) { + Add-AppEventLogSource + } + + + # The core already had a final value before the crash, so the crash cannot have been caused by it + if (Test-CoreIsResolved $CoreFromAutoMode) { + Write-ColorText('Core ' + $CoreFromAutoMode + ' already had a final value before the crash, the crash cannot be attributed to it') Yellow + Write-ColorText('Continuing with the remaining cores') Yellow + } + else { + Write-VerboseText('Adjusting the ' + $modeDescription + ' voltage value') + + # We need to pass empty values at this point, as we're only adjusting the starting values + $params = @{ + 'mode' = 'RESUME' + 'actualCoreNumber' = $CoreFromAutoMode + } + + Test-AutomaticTestModeIncrease @params + } + + + # Store the state of this resume attempt before any value is applied to the processor + # If we crash while applying them, the increased value resp. the increased resume counter is already on the disk, + # otherwise a value that crashes the machine before the first core can be started would be re-applied on every + # single reboot, without the counter above ever moving forward + Save-AutoModeState + } + + # Apply the starting values # Do these after the startup task has been created # But only if not settint the voltage just for the currently tested core @@ -5740,6 +6927,12 @@ function Set-CurveOptimizerValues { if ($i -eq $Script:currentlyTestedCore) { $voltageValuesToUse += [Int] $voltageCurrentValues[$i] } + + # If the setting is enabled, cores with an already confirmed value keep that value + elseif ($applyConfirmedValuesForNotTestedCores -and $useAutomaticTestMode -and $coreStates.ContainsKey($i) -and $coreStates[$i]['status'] -eq 'confirmed') { + $voltageValuesToUse += [Int] $voltageCurrentValues[$i] + } + else { # We may have allowed higher values than 0 $voltageValuesToUse += [Math]::Max($voltageValueForNotTestedCores, $voltageCurrentValues[$i]) @@ -10550,16 +11743,20 @@ function Test-AutomaticTestModeIncrease { if ($oldValue -ge $maxValue) { Write-ColorText('Cannot increase the ' + $autoModeDescription + ' value for core ' + $actualCoreNumber + ' anymore! The maximum of ' + $maxValueStr + ' has been ' + $reachedOrExceeded) DarkYellow - [Void] $Script:coresWithIncreasedVoltageValue.Add($actualCoreNumber) - [Void] $Script:coresWithErrorAndMaxVoltageValue.Add($actualCoreNumber) - - if ($settings.General.skipCoreOnError) { - Write-ColorText('This core will now be skipped in the following iterations') Yellow + if ($Script:coresWithIncreasedVoltageValue -notcontains $actualCoreNumber) { + [Void] $Script:coresWithIncreasedVoltageValue.Add($actualCoreNumber) } + + # This core cannot be stabilized anymore, it will not be tested again + Reset-CorePasses -coreNumber $actualCoreNumber -CountError + Set-CoreState -coreNumber $actualCoreNumber -status 'unstable' -value $oldValue -reason 'maxValue' } else { $Script:voltageCurrentValues[$actualCoreNumber] = $newValue - [Void] $Script:coresWithIncreasedVoltageValue.Add($actualCoreNumber) + + if ($Script:coresWithIncreasedVoltageValue -notcontains $actualCoreNumber) { + [Void] $Script:coresWithIncreasedVoltageValue.Add($actualCoreNumber) + } # The current Intel implementation only works with a single voltage offset, not per core if ($useIntelVoltageAdjustment) { @@ -10574,10 +11771,38 @@ function Test-AutomaticTestModeIncrease { Write-ColorText('This is the maximum set ' + $autoModeDescription + ' value, there will be no further increases') Yellow } + if ($canUseWindowsEventLog) { + Write-AppEventLog -type $logType -infoString1 $actualCoreNumber -infoString2 $oldValue -infoString3 $newValue + } + + + # The value for this core has changed, so it needs to collect its error free test runs from scratch again + Reset-CorePasses -coreNumber $actualCoreNumber -CountError + + # For Intel the single voltage offset applies to all of the cores, so no core can keep its collected passes + if ($useIntelVoltageAdjustment) { + for ($i = 0; $i -lt $Script:voltageCurrentValues.Count; $i++) { + if ($i -ne $actualCoreNumber) { + Reset-CorePasses -coreNumber $i + } + } + } + + $resultLine = (Get-Date -Format 'yyyy-MM-dd HH:mm:ss') + ' Core ' + $actualCoreNumber.ToString().PadLeft(3, ' ') + ' ' + 'ERROR'.PadRight(9, ' ') + $resultLine += ' ' + $(if ($useCurveOptimizer) { 'CO' } else { 'mV' }) + ' ' + $oldValue.ToString().PadLeft(5, ' ') + ' -> ' + $newValue.ToString() + $resultLine += ' (error ' + $(if ($coreStates.ContainsKey($actualCoreNumber)) { $coreStates[$actualCoreNumber]['errors'] } else { 0 }) + ', pass counter reset)' + + Write-AutoModeResultEntry -text $resultLine -NoLogEntry + + # The value for this core has actually been changed, so a resume that got us here has made progress + $Script:autoModeResumeAttempts = 0 + + Set-CoreState -coreNumber $actualCoreNumber -status 'testing' -value $newValue + # Apply the new values + # This happens only after the new value has been stored, so that a crash while it is being applied cannot + # make the script set the same crashing value again after the resume Set-NewVoltageValues - - Write-AppEventLog -type $logType -infoString1 $actualCoreNumber -infoString2 $oldValue -infoString3 $newValue } @@ -10639,6 +11864,27 @@ function Get-NewLogfileEntries { return } + # The log file has become smaller than before, so it must have been re-created or truncated + # If we don't reset our stored positions here, we would never look at this file again, and so no passed + # test would ever be detected anymore + if ($null -ne $previousFileSize -and $resultFileHandle.Length -lt $previousFileSize) { + Write-DebugText(' The stress test log file has become smaller, it seems to have been re-created') + Write-DebugText(' Resetting the stored file positions') + + $Script:previousFileSize = 0 + $Script:lastFilePosition = 0 + $Script:lineCounter = 0 + $Script:previousPassedFFTEntry = $null + $Script:previousPassedFFTSize = $null + $Script:previousPassedTestEntry = $null + $Script:previousPassedTest = $null + + # The stored FFT lines are also gone with the old file, and their line numbers are being re-used by the new one + # If we kept them, a line of the new log could be matched against a line of the old one and so be seen as the + # second entry of a "pair" for two threads, counting a passed FFT size that only one worker has completed + $Script:allFFTLogEntries = [System.Collections.ArrayList]::new() + } + # Only perform the check if the file size has increased # The size has increased, so something must have changed # It's either a new passed FFT entry, a [Timestamp], or an error @@ -11332,6 +12578,21 @@ function Write-AppEventLog { 'entryType' = 'Information' 'message' = [String]::Format('Increasing voltage offset value from {1} to {2} (error on core {0})', $infoString1, $infoString2, $infoString3) } + 'core_co_known' = @{ + 'eventId' = 5050 + 'entryType' = 'Information' + 'message' = [String]::Format('Core {0}: using the value {1} provided in the config file', $infoString1, $infoString2) + } + 'core_co_confirmed' = @{ + 'eventId' = 5100 + 'entryType' = 'Information' + 'message' = [String]::Format('Core {0}: the value {1} has been confirmed ({2})', $infoString1, $infoString2, $infoString3) + } + 'core_co_unstable' = @{ + 'eventId' = 5200 + 'entryType' = 'Warning' + 'message' = [String]::Format('Core {0}: no stable value could be found (last value {1}, reason: {2})', $infoString1, $infoString2, $infoString3) + } 'core_whea' = @{ 'eventId' = 8888 'entryType' = 'Warning' @@ -13000,7 +14261,10 @@ try { # Always remove the .automode file at this point, we don't want it to interfere - Remove-AutoModeFile + # But not when we're resuming after a crash, we'd lose the last valid state until the first core has been started + if (!$useAutomaticTestModeWithResume -or $CoreFromAutoMode -lt 0) { + Remove-AutoModeFile + } @@ -13058,7 +14322,7 @@ try { Write-SettingIntroText -Text 'Suspend periodically' -Setting ($(if ($settings.General.suspendPeriodically) { 'ENABLED' } else { 'DISABLED' })) Write-SettingIntroText -Text 'Restart for each core' -Setting ($(if ($settings.General.restartTestProgramForEachCore) { 'ENABLED' } else { 'DISABLED' })) Write-SettingIntroText -Text 'Test order of cores' -Setting ($settings.General.coreTestOrder.ToUpperInvariant() + $(if ($settings.General.coreTestOrder.ToLowerInvariant() -eq 'default') { ' (' + $coreTestOrderMode.ToUpperInvariant() + ')' })) - Write-SettingIntroText -Text 'Number of iterations' -Setting ($settings.General.maxIterations) + Write-SettingIntroText -Text 'Number of iterations' -Setting ($settings.General.maxIterations.ToString() + $(if ($useAutomaticTestMode) { ' (not used in Automatic Test Mode)' })) # Print a message if we're ignoring certain cores @@ -13081,9 +14345,16 @@ try { Write-SettingIntroText -Text 'Starting Curve Optimizer values' -Setting ($voltageStartingValues -Join ', ') } + Write-SettingIntroText -Text 'Passes to confirm a value' -Setting ($passesToConfirmCoreValue) + + if ($knownGoodValues.Count -gt 0) { + Write-SettingIntroText -Text 'Known good values (not tested)' -Setting (Get-ConfirmedValuesString -OnlyFromConfig) + } + if ($useCurveOptimizer -and $setVoltageOnlyForTestedCore) { Write-SettingIntroText -Text 'Set voltage only for the tested core' -Setting ('ENABLED') Write-SettingIntroText -Text 'The voltage for the untested cores' -Setting ($voltageValueForNotTestedCores) + Write-SettingIntroText -Text 'Apply confirmed values to other cores' -Setting ($(if ($applyConfirmedValuesForNotTestedCores) { 'ENABLED' } else { 'DISABLED' })) } if ($useIntelVoltageAdjustment) { @@ -13121,6 +14392,10 @@ try { Write-ColorText((' - ' + $stressTestPrograms[$settings.General.stressTestProgram]['displayName'] + ':').PadRight($leftStringLength, ' ') + $stressTestLogFileName) Cyan } + if ($useAutomaticTestMode -and $autoModeResultsFileName) { + Write-ColorText((' - Auto Mode:').PadRight($leftStringLength, ' ') + $autoModeResultsFileName) Cyan + } + Write-ColorText('────────────────────────────────────────────────────────────────────────────────') Cyan Write-Text('') @@ -13315,6 +14590,10 @@ try { $infoString += $logFilePathAbsolute + $stressTestLogFileName + [Environment]::NewLine } + if ($useAutomaticTestMode -and $autoModeResultsFileName) { + $infoString += $logFilePathAbsolute + $autoModeResultsFileName + [Environment]::NewLine + } + $infoString += [Environment]::NewLine $infoString += ('Stress test program: ' + $selectedStressTestProgram.ToUpperInvariant() + [Environment]::NewLine) $infoString += ('Selected test mode: ' + $settings.mode.ToUpperInvariant() + [Environment]::NewLine) @@ -13389,29 +14668,21 @@ try { # Remove ignored cores [System.Collections.ArrayList] $coresToTest = @($coresToTest | Where-Object { $_ -NotIn $settings.General.coresToIgnore }) + # There needs to be at least one core to test in the first place + # Otherwise we would report a finished run in Automatic Test Mode without having tested anything at all + if (@($coresToTest).Count -lt 1) { + Exit-WithFatalError('No valid core to test selected!') + } - # Add the previously tested core from before the reboot if we're in Automatic Test Mode with resume - if ($useAutomaticTestModeWithResume -and $CoreFromAutoMode -gt -1) { - $timestamp = Get-Date -Format HH:mm:ss - Write-DebugText($timestamp) - Write-Text('') - Write-ColorText('Apparently the computer crashed in the last run while testing core ' + $CoreFromAutoMode) Red - Write-ColorText('Trying to resume the test process') Red - - Write-VerboseText('Adding core ' + $CoreFromAutoMode + ' to the front of the test array') - [Void] $coresToTest.Insert(0, $CoreFromAutoMode) - - $modeDescription = $(if ($useCurveOptimizer) { 'Curve Optimizer' } else { 'voltage offset' }) - Write-VerboseText('Adjusting the ' + $modeDescription + ' voltage value') + # Remove the cores that already have a final Curve Optimizer / voltage offset value + if ($useAutomaticTestMode) { + [System.Collections.ArrayList] $coresToTest = @($coresToTest | Where-Object { !(Test-CoreIsResolved ([Int] $_)) }) + } - # We need to pass empty values at this point, as we're only adjusting the starting values - $params = @{ - 'mode' = 'RESUME' - 'actualCoreNumber' = $CoreFromAutoMode - } - Test-AutomaticTestModeIncrease @params - } + # Note: The value of the previously tested core from before the reboot has already been adjusted in + # Initialize-AutomaticTestMode, it needs to happen before any value is applied to the processor + # The core itself is put back to the front of the test order via the persisted remaining test order (see #106) Write-VerboseText('All cores that could be tested:') Write-VerboseText($allCores -Join ', ') @@ -13421,18 +14692,19 @@ try { # Start with the CPU test # Repeat the whole check $settings.General.maxIterations times - for ($iteration = 1; $iteration -le $settings.General.maxIterations; $iteration++) { + # In Automatic Test Mode we ignore maxIterations and instead run until every core has a final value + for ($iteration = $resumedIteration; $useAutomaticTestMode -or $iteration -le $settings.General.maxIterations; $iteration++) { $timestamp = Get-Date -Format HH:mm:ss # Define the available cores [System.Collections.ArrayList] $coreTestOrderArray = $coresToTest.Clone() - $halfCores = $numPhysCores / 2 + # For an odd number of physical cores the first half is one core larger, so that all of the cores are covered + $halfCores = [Int] [Math]::Ceiling($numPhysCores / 2) $numAvailableCores = $coreTestOrderArray.Count $numUniqueAvailableCores = @($coreTestOrderArray | Sort-Object | Get-Unique).Count $numCoresWithError = $coresWithError.Count $numCoresWithWheaError = $coresWithWheaError.Count - $previousCoreNumber = $null # Check if all of the cores have thrown an error, and if so, abort @@ -13447,14 +14719,15 @@ try { } - # Show a different error message if we're using Automatic Test Mode and all cores have reached their maximum value - if ($useAutomaticTestMode -and $numCoresWithErrorAndMaxVoltageValue -gt 0 -and $numCoresWithErrorAndMaxVoltageValue -eq $numUniqueAvailableCores) { - Close-StressTestProgram - - $autoModeDescription = $(if ($useCurveOptimizer) { 'Curve Optimizer' } elseif ($useIntelVoltageAdjustment) { 'voltage offset' }) + # In Automatic Test Mode, check if there is anything left to do at all + # A core that has found a good value or that couldn't be stabilized at all doesn't need to be tested anymore + if ($useAutomaticTestMode) { + $unresolvedCores = @(@($coresToTest | Sort-Object -Unique) | Where-Object { !(Test-CoreIsResolved ([Int] $_)) }) - Write-ColorText($timestamp + ' - All Cores have reached the maximum ' + $autoModeDescription + ' value and thrown an error, aborting!') Yellow - Exit-Script -errorCode 5 + if ($unresolvedCores.Count -lt 1) { + Write-VerboseText('All cores have a final value, ending the test process') + break + } } @@ -13487,30 +14760,18 @@ try { # Start fresh $coreTestOrderArray = [System.Collections.ArrayList]::new() - # If we had added a core from CoreFromAutoMode, we will need to push it to the front here again - if ($useAutomaticTestModeWithResume -and $CoreFromAutoMode -gt -1) { - [Void] $coreTestOrderArray.Add($CoreFromAutoMode) - } - # 0, $halfCores, 0+1, $halfCores+1, ... + # With an odd number of physical cores the second half has one core less, so the last entry doesn't exist # TODO: Maybe find a better way to handle ignored cores, so that there's still an alternation, instead of just skipping it - for ($i = 0; $i -lt $numPhysCores; $i++) { - $currentCoreNumber = 0 - - if ($null -ne $previousCoreNumber) { - if ($previousCoreNumber -lt $halfCores) { - $currentCoreNumber = [Int] ($previousCoreNumber + $halfCores) - } - else { - $currentCoreNumber = [Int] ($previousCoreNumber - $halfCores + 1) + for ($i = 0; $i -lt $halfCores; $i++) { + foreach ($currentCoreNumber in @($i, ($i + $halfCores))) { + if ($currentCoreNumber -gt ($numPhysCores-1)) { + continue } - } - - $previousCoreNumber = $currentCoreNumber - - if (!$settings.General.coresToIgnore.Contains($currentCoreNumber)) { - [Void] $coreTestOrderArray.Add($currentCoreNumber) + if (!$settings.General.coresToIgnore.Contains($currentCoreNumber)) { + [Void] $coreTestOrderArray.Add($currentCoreNumber) + } } } } @@ -13524,29 +14785,6 @@ try { Write-DebugText('The randomized test order:') Write-DebugText($coreTestOrderArray -Join ', ') - - # If we had added a core from CoreFromAutoMode, push that core to the front - if ($useAutomaticTestModeWithResume -and $CoreFromAutoMode -gt -1) { - Write-VerboseText('Moving the passed core to the beginning of the test order') - - [System.Collections.ArrayList] $coreTestOrderArrayOri = $coreTestOrderArray.Clone() - [System.Collections.ArrayList] $coreTestOrderArray = @() - - $coreTestOrderArrayOri | ForEach-Object { - if ([Int] $_ -eq [Int] $CoreFromAutoMode) { - [Void] $coreTestOrderArray.Insert(0, [Int] $_) - } - else { - [Void] $coreTestOrderArray.Add([Int] $_) - } - } - - $numAvailableCores = $coreTestOrderArray.Count - $numUniqueAvailableCores = @($coreTestOrderArray | Sort-Object | Get-Unique).Count - - Write-VerboseText('The test order with the core moved to the front:') - Write-VerboseText($coreTestOrderArray -Join ', ') - } } # Go through core combinations @@ -13574,18 +14812,6 @@ try { } } - # If we had added a core from CoreFromAutoMode, push that core to the front - # Eventually this should be the pair that crashed, but currently we only support single cores - # TODO for a future revision - if ($useAutomaticTestModeWithResume -and $CoreFromAutoMode -gt -1) { - Write-VerboseText('Moving the passed core to the beginning of the test order') - Write-DebugText('Eventually this should be the core pair that failed, but we don''t support that yet') - [Void] $coreTestOrderArray.Insert(0, [Int] $_) - } - - $numAvailableCores = $coreTestOrderArray.Count - $numUniqueAvailableCores = @($coreTestOrderArray | Sort-Object | Get-Unique).Count - Write-DebugText('The core pairs test order:') Write-DebugText($coreTestOrderArray -Join ', ') } @@ -13604,6 +14830,40 @@ try { # It also already doesn't include the ignored cores } + # Automatic Test Mode: continue where we left off before a crash and skip the cores with a final value + if ($useAutomaticTestMode) { + # Continue with the not yet tested cores of the interrupted iteration (see #106) + if ($useResumedCoreOrder) { + # The number of cores may have changed since the crash (e.g. a CCD was disabled in the BIOS), and some + # of the cores may have gotten a final value in the meantime, so only keep the ones we can still test + $restoredCoreOrder = @(@($resumedCoreOrder) | ForEach-Object { [Int] $_ } | Where-Object { $coreStates.ContainsKey($_) -and !(Test-CoreIsResolved $_) }) + + if ($restoredCoreOrder.Count -gt 0) { + Write-VerboseText('Resuming the interrupted test order from before the crash') + [System.Collections.ArrayList] $coreTestOrderArray = $restoredCoreOrder + } + else { + # The crash happened on the last core of the iteration, just use the regular test order + Write-VerboseText('The stored remaining test order was empty, using the regular test order') + } + + $useResumedCoreOrder = $false + $resumedCoreOrder = @() + } + + # Cores with a final Curve Optimizer / voltage offset value are never tested again + [System.Collections.ArrayList] $coreTestOrderArray = @(@($coreTestOrderArray) | Where-Object { !(Test-CoreIsResolved ([Int] $_)) }) + + Write-VerboseText('Cores with a final value (skipped): ' + ((@($coreStates.Keys) | Where-Object { Test-CoreIsResolved ([Int] $_) } | Sort-Object) -Join ', ')) + } + + + # The core order builders above may have changed the array, so (re)calculate the counts here, once, for every mode + $numAvailableCores = @($coreTestOrderArray).Count + $numUniqueAvailableCores = @(@($coreTestOrderArray) | Sort-Object | Get-Unique).Count + $numCoresWithErrorAndMaxVoltageValue = @(@($coresWithErrorAndMaxVoltageValue) | Sort-Object | Get-Unique).Count + + Write-VerboseText('The final test order:') Write-VerboseText($coreTestOrderArray -Join ', ') @@ -13611,8 +14871,11 @@ try { Write-DebugText('The number of unique available cores: ' + $numUniqueAvailableCores) Write-DebugText('The number of cores with an error: ' + $numCoresWithError) Write-DebugText('The number of cores with a WHEA error: ' + $numCoresWithWheaError) + Write-DebugText('The number of cores at the max value: ' + $numCoresWithErrorAndMaxVoltageValue) + # In Automatic Test Mode the cores that still have to be tested have already been checked above, so if the + # built test order is empty at this point, the selected test order cannot cover them at all if (@($coreTestOrderArray).Count -lt 1) { Exit-WithFatalError('No valid core to test selected!') } @@ -13754,9 +15017,26 @@ try { } - # Also skip this core if the maximum Curve Optimizer / voltage offset value has been reached - if ($useAutomaticTestMode -and $settings.General.skipCoreOnError -and $coresWithErrorAndMaxVoltageValue -contains $actualCoreNumber) { - Write-Text($timestamp + ' - Core ' + $actualCoreNumber + ' (CPU ' + $cpuNumberString + ') has reached the maximum value and has previously thrown an error, skipping') + # Also skip this core if it already has a final Curve Optimizer / voltage offset value + # A core can become "resolved" after the test order for this iteration has been built, so we need to check this here as well + if ($useAutomaticTestMode -and (Test-CoreIsResolved $actualCoreNumber)) { + $autoModeDescription = $(if ($useCurveOptimizer) { 'Curve Optimizer' } else { 'voltage offset' }) + $thisCoreState = $coreStates[$actualCoreNumber] + $skipReason = '' + + if ($thisCoreState['status'] -eq 'confirmed') { + $skipReason = 'already has a final ' + $autoModeDescription + ' value of ' + $thisCoreState['value'] + $skipReason += $(if ($thisCoreState['source'] -eq 'config') { ' (provided in the config file)' }) + $skipReason += ', skipping' + } + elseif ($thisCoreState['status'] -eq 'unstable') { + $skipReason = 'could not be stabilized (' + $(if ($thisCoreState['reason'] -eq 'maxValue') { 'the maximum value was reached' } else { 'the internal safety limit was reached' }) + '), skipping' + } + else { + $skipReason = 'is being ignored, skipping' + } + + Write-Text($timestamp + ' - Core ' + $actualCoreNumber + ' (CPU ' + $cpuNumberString + ') ' + $skipReason) # Remove this core from the array of still available cores [Void] $coreTestOrderArray.RemoveAt(0) @@ -13789,6 +15069,12 @@ try { Write-VerboseText('Skipping this core due to Aida64 not running correctly on Core 0 / CPU 0 and Hyperthreading / SMT is disabled') + # This core can never be tested, so in Automatic Test Mode we must not wait for a result for it, + # otherwise the test process would never end + if ($useAutomaticTestMode) { + Set-CoreState -coreNumber $actualCoreNumber -status 'ignored' + } + # Remove this core from the array of still available cores [Void] $coreTestOrderArray.RemoveAt(0) continue @@ -13858,6 +15144,22 @@ try { [Void] $coreTestOrderArray.RemoveAt(0) + # Automatic Test Mode: count the test runs for this core and check the internal safety limit + # This should never trigger, it only exists so that a single core cannot be tested indefinitely + if ($useAutomaticTestMode -and $coreStates.ContainsKey($actualCoreNumber)) { + $coreStates[$actualCoreNumber]['tests']++ + + if ($coreStates[$actualCoreNumber]['tests'] -gt $maxTestsPerCoreSanityLimit) { + Write-ColorText($timestamp + ' - Core ' + $actualCoreNumber + ' has been tested ' + $coreStates[$actualCoreNumber]['tests'] + ' times without a result, this exceeds the internal safety limit of ' + $maxTestsPerCoreSanityLimit) Red + + Set-CoreState -coreNumber $actualCoreNumber -status 'unstable' -reason 'sanityLimit' + continue + } + + Set-CoreState -coreNumber $actualCoreNumber -status 'testing' + } + + # This core has not thrown an error yet, run the test $startCoreDate = Get-Date $timestamp = $startCoreDate.ToString('HH:mm:ss') @@ -13876,8 +15178,11 @@ try { # Set the .automode file if we're in Automatic Test Mode with resuming + # This flags this core as the one that is now being tested, and it has to happen before its (aggressive) + # value is applied to the CPU further below, otherwise a crash would be attributed to the previous core + # We also store the remaining test order, so that we can continue where we left off after a crash (see #106) if ($useAutomaticTestModeWithResume) { - Set-AutoModeFile $actualCoreNumber + Save-AutoModeState -coreNumber $actualCoreNumber -iterationNumber $iteration -remainingCoreOrder (@($actualCoreNumber) + @($coreTestOrderArray)) } @@ -14026,7 +15331,16 @@ try { $totalRuntimeArray += ($coreStartDifference.Seconds.ToString().PadLeft(2, '0') + 's') $totalRunTimeString = $totalRuntimeArray -Join ' ' - Write-ColorText(' Progress ' + ($coreIndex+1) + '/' + $numAvailableCores + ' | Iteration ' + $iteration + '/' + $settings.General.maxIterations + ' | Runtime ' + $totalRunTimeString) DarkGray + # In Automatic Test Mode the maxIterations setting is not used, so display the confirmation progress instead + if ($useAutomaticTestMode) { + $numConfirmedCores = @(@($coreStates.GetEnumerator()) | Where-Object { $_.Value['status'] -eq 'confirmed' }).Count + $passesForThisCore = $coreStates[$actualCoreNumber]['passes'] + + Write-ColorText(' Progress ' + ($coreIndex+1) + '/' + $numAvailableCores + ' | Iteration ' + $iteration + ' | Pass ' + ($passesForThisCore+1) + '/' + $passesToConfirmCoreValue + ' | Confirmed ' + $numConfirmedCores + '/' + $numPhysCores + ' | Runtime ' + $totalRunTimeString) DarkGray + } + else { + Write-ColorText(' Progress ' + ($coreIndex+1) + '/' + $numAvailableCores + ' | Iteration ' + $iteration + '/' + $settings.General.maxIterations + ' | Runtime ' + $totalRunTimeString) DarkGray + } Write-DebugText('The number of cores with an error so far: ' + $numCoresWithError) if ($settings.General.lookForWheaErrors) { @@ -14426,6 +15740,11 @@ try { Write-AppEventLog -type 'core_finished' -infoString1 $coreString -infoString2 ('Test completed in ' + $runTimeStringCore) } + # This core has completed a test run without an error + if ($useAutomaticTestMode) { + Add-CorePass $actualCoreNumber + } + continue LoopCoreRunner } @@ -14659,6 +15978,11 @@ try { Write-AppEventLog -type 'core_finished' -infoString1 $coreString -infoString2 ('Test completed in ' + $runTimeStringCore) } + # This core has completed a test run without an error + if ($useAutomaticTestMode) { + Add-CorePass $actualCoreNumber + } + continue LoopCoreRunner } @@ -14755,6 +16079,11 @@ try { if ($canUseWindowsEventLog) { Write-AppEventLog -type 'core_finished' -infoString1 $coreString -infoString2 ('Test completed in ' + $runTimeStringCore) } + + # This core has completed a test run without an error + if ($useAutomaticTestMode) { + Add-CorePass $actualCoreNumber + } } # End: :LoopCoreRunner for ($coreIndex = 0; $coreIndex -lt $numAvailableCores; $coreIndex++) @@ -14810,6 +16139,30 @@ try { } + # Show which cores already have a final value and which ones are still being tested + if ($useAutomaticTestMode) { + $resolvedCoresStrings = @() + $remainingCoresArray = @() + + foreach ($entry in @($coreStates.GetEnumerator() | Sort-Object -Property { [Int] $_.Name })) { + if ($entry.Value['status'] -eq 'ignored') { + continue + } + + if (Test-CoreIsResolved ([Int] $entry.Name)) { + $resolvedCoresStrings += ([Int] $entry.Name).ToString() + ' (' + $entry.Value['value'] + ')' + } + else { + $remainingCoresArray += ([Int] $entry.Name).ToString() + } + } + + Write-ColorText('Cores with a final value: ' + $(if ($resolvedCoresStrings.Count -gt 0) { $resolvedCoresStrings -Join ', ' } else { '(none)' })) Cyan + Write-ColorText('Remaining cores: ' + $(if ($remainingCoresArray.Count -gt 0) { $remainingCoresArray -Join ', ' } else { '(none)' })) Cyan + Write-Text('') + } + + # Show the starting and current Curve Optimizer values if ($useCurveOptimizer -and $numCoresWithIncreasedVoltageValue -gt 0) { $coCoresString = ((0..($numPhysCores-1)) | ForEach-Object { ('C' + $_.ToString()).PadLeft(4, ' ') }) -Join ' |' @@ -14834,6 +16187,74 @@ try { # The CoreCycler has finished $timestamp = Get-Date -Format HH:mm:ss + + if ($useAutomaticTestMode) { + $autoModeDescription = $(if ($useCurveOptimizer) { 'Curve Optimizer' } else { 'voltage offset' }) + + # The cores that were selected for this run + # A custom test order can limit the run to only some of the cores, in which case we cannot make a statement about the others + $coresForThisRun = @(@(0..($numPhysCores-1)) | Where-Object { $_ -NotIn $settings.General.coresToIgnore }) + $coresDescription = 'All cores' + + if ($coreTestOrderMode -eq 'custom') { + $coresForThisRun = @(@($coreTestOrderCustom | Sort-Object -Unique) | Where-Object { $_ -NotIn $settings.General.coresToIgnore }) + $coresDescription = 'All of the selected cores (' + ($coresForThisRun -Join ', ') + ')' + } + + # Cores that had to be skipped while the test was already running can never get a value + # (e.g. Aida64 on Core 0 with disabled Hyperthreading / SMT), and they are not part of the coresToIgnore setting, + # so we cannot report a finished run for them either + $skippedCores = @(@($coresForThisRun) | Where-Object { $coreStates.ContainsKey([Int] $_) -and $coreStates[[Int] $_]['status'] -eq 'ignored' }) + + if ($skippedCores.Count -gt 0) { + $coresForThisRun = @(@($coresForThisRun) | Where-Object { $skippedCores -NotContains $_ }) + $coresDescription = 'All of the tested cores (' + ($coresForThisRun -Join ', ') + ')' + } + + $confirmedCores = @(@($coresForThisRun) | Where-Object { $coreStates.ContainsKey([Int] $_) -and $coreStates[[Int] $_]['status'] -eq 'confirmed' }) + $unstableCores = @(@($coresForThisRun) | Where-Object { $coreStates.ContainsKey([Int] $_) -and $coreStates[[Int] $_]['status'] -eq 'unstable' }) + $coresWithoutFinalValue = @(@($coresForThisRun) | Where-Object { !(Test-CoreIsResolved ([Int] $_)) }) + + # Only report a completed run if the cores really do have a final value + if ($coresForThisRun.Count -lt 1) { + Write-ColorText($timestamp + ' - The test process has ended, but not a single core could be tested!') Yellow + } + elseif ($coresWithoutFinalValue.Count -gt 0) { + Write-ColorText($timestamp + ' - The test process has ended, but not all of the cores have a final ' + $autoModeDescription + ' value!') Yellow + Write-ColorText('The following cores are still without a final value: ' + ($coresWithoutFinalValue -Join ', ')) Yellow + } + elseif ($unstableCores.Count -gt 0) { + Write-ColorText($timestamp + ' - CoreCycler finished, but ' + $unstableCores.Count + ' core' + $(if ($unstableCores.Count -gt 1) { 's' }) + ' could not be stabilized: ' + ($unstableCores -Join ', ')) Yellow + } + else { + Write-ColorText($timestamp + ' - ' + $coresDescription + ' have a final ' + $autoModeDescription + ' value - CoreCycler finished!') Green + } + + if ($skippedCores.Count -gt 0) { + Write-ColorText('The following cores had to be skipped during the run and have no ' + $autoModeDescription + ' value: ' + ($skippedCores -Join ', ')) Yellow + } + + if ($canUseWindowsEventLog) { + Write-AppEventLog -type 'script_finished' + } + + Close-StressTestProgram + + # Not a single core could be stabilized + if ($confirmedCores.Count -lt 1 -and $unstableCores.Count -gt 0) { + Write-ColorText($timestamp + ' - All Cores have reached the maximum ' + $autoModeDescription + ' value and thrown an error, aborting!') Yellow + Exit-Script -errorCode 5 + } + + # This should never happen, the test process only ends when every core has a final value + # It also covers the case where not a single core of the test order could be tested at all + if ($coresWithoutFinalValue.Count -gt 0 -or $coresForThisRun.Count -lt 1) { + Exit-Script -errorCode 6 + } + + Exit-Script + } + Write-ColorText($timestamp + ' - CoreCycler finished!') Green if ($canUseWindowsEventLog) { @@ -14919,6 +16340,10 @@ finally { } + # Store the found Curve Optimizer / voltage offset values permanently, no matter how the script ended + Write-AutoModeResultsFooter + + if ($canUseWindowsEventLog) { $infoString = 'The log files for this run are stored in:' + [Environment]::NewLine $infoString += $logFilePathAbsolute + $logFileName + [Environment]::NewLine @@ -14927,6 +16352,10 @@ finally { $infoString += $logFilePathAbsolute + $stressTestPrograms[$settings.General.stressTestProgram]['displayName'] + ': ' + $stressTestLogFileName } + if ($useAutomaticTestMode -and $autoModeResultsFileName) { + $infoString += [Environment]::NewLine + 'Auto Mode: ' + $logFilePathAbsolute + $autoModeResultsFileName + } + $finalSummary = (Show-FinalSummary -ReturnText) Write-AppEventLog -type 'script_terminated' -infoString1 $infoString -infoString2 $finalSummary From 87c54c52a84ae579973c9d3054274d8df3519f3b Mon Sep 17 00:00:00 2001 From: Kristoffer Konstad Hansen Date: Tue, 28 Jul 2026 15:56:49 +0200 Subject: [PATCH 2/4] Don't silently attribute an unidentifiable WHEA error to core 0 Convert-WheaMessageToApicId returns a negative value when the APIC ID cannot be determined: -1 for every event id other than 18 and 19, -2 when the entry has no Properties member at all. Convert-WheaMessageToCoreId then passed that value straight into the $coresInfo['apicIdToCore'] hash table. A key that doesn't exist returns $null, and [Int] $null is 0, so every WHEA entry that cannot be attributed to a core was silently reported as core 0. With treatWheaWarningAsError = 1 that had two consequences: - While core 0 was being tested, an unrelated WHEA entry (e.g. event id 47, "corrected machine check", component "Memory") was counted as a real error for core 0. - While any other core was being tested, the same entry printed "the APIC ID from the WHEA message does not match the tested core" with an APIC ID of -1 and the test continued as if nothing had happened. The same happened for a valid APIC ID that isn't in the map, which can occur on processors whose APIC IDs have a gap (e.g. a Ryzen 5900X). Convert-WheaMessageToCoreId now returns -1 in these cases instead, and the WHEA check treats an entry without a usable core as an error for the tested core, because it cannot be ruled out that it caused it. A negative APIC ID / core is displayed as "not available" instead of as a number. Co-Authored-By: Claude Opus 5 --- configs/default.config.ini | 2 ++ script-corecycler.ps1 | 37 ++++++++++++++++++++++++++++++------- 2 files changed, 32 insertions(+), 7 deletions(-) diff --git a/configs/default.config.ini b/configs/default.config.ini index 6745189..11da466 100644 --- a/configs/default.config.ini +++ b/configs/default.config.ini @@ -226,6 +226,8 @@ lookForWheaErrors = 1 # The testing on the core will be stopped and continued on the next one # However only if the APIC ID from the WHEA message matches the core that was currently tested, otherwise # only a warning will be displayed +# Some WHEA entries don't contain an APIC ID at all (e.g. a corrected memory error). As the tested core cannot be +# ruled out for those, they are also treated as an error # # Default: 1 treatWheaWarningAsError = 1 diff --git a/script-corecycler.ps1 b/script-corecycler.ps1 index 3be3a3c..bc3fe40 100644 --- a/script-corecycler.ps1 +++ b/script-corecycler.ps1 @@ -439,6 +439,8 @@ lookForWheaErrors = 1 # The testing on the core will be stopped and continued on the next one # However only if the APIC ID from the WHEA message matches the core that was currently tested, otherwise # only a warning will be displayed +# Some WHEA entries don't contain an APIC ID at all (e.g. a corrected memory error). As the tested core cannot be +# ruled out for those, they are also treated as an error # # Default: 1 treatWheaWarningAsError = 1 @@ -9510,9 +9512,13 @@ function Test-StressTestProgrammIsRunning { Write-DebugText('The core extracted APIC ID from the WHEA message: ' + $apicIdFromWheaMessage) Write-DebugText('The core extracted from the WHEA message: ' + $coreFromWheaMessage) + # A negative value means that the entry didn't contain the information, don't display it as a number + $apicIdString = $(if ($apicIdFromWheaMessage -lt 0) { 'not available' } else { $apicIdFromWheaMessage.ToString() }) + $coreIdString = $(if ($coreFromWheaMessage -lt 0) { 'not available' } else { $coreFromWheaMessage.ToString() }) + # Depending on the settings, this is either a warning or an error - # However it is only an error if the core matches the APIC ID + # However it is only an error if the core matches the APIC ID, or if the APIC ID is not available at all if ($settings.General.treatWheaWarningAsError -gt 0) { if ($actualCoreNumber -eq $coreFromWheaMessage) { Write-DebugText('The core from the WHEA message matches the tested core (' + $coreNumber + ' = ' + $coreFromWheaMessage + ')') @@ -9521,6 +9527,14 @@ function Test-StressTestProgrammIsRunning { $errorType = 'WHEAERROR' } + # No core in the WHEA message, so we cannot rule out that the tested core has caused it + elseif ($coreFromWheaMessage -lt 0) { + Write-DebugText('The WHEA message does not contain a core, treating it as an error for the tested core') + + $stressTestError = 'There has been a WHEA error while running this test and the setting to treat this as an error has been set! The WHEA entry does not contain the core that caused it, so the tested core cannot be ruled out.' + $errorType = 'WHEAERROR' + } + # Wrong core, just display a warning else { Write-DebugText('The core from the WHEA message does not match the tested core (' + $coreNumber + ' = ' + $coreFromWheaMessage + ')') @@ -9531,7 +9545,7 @@ function Test-StressTestProgrammIsRunning { Write-ColorText('WARNING: The flag to treat this as an error has been set, but the APIC ID from the WHEA message does not match the tested core!') Magenta Write-ColorText('WHEA TIMESTAMP: ' + $lastWheaError.TimeCreated.ToString()) Magenta Write-ColorText('WHEA EVENT ID: ' + $lastWheaError.Id) Magenta - Write-ColorText('WHEA APIC ID: ' + $apicIdFromWheaMessage) Magenta + Write-ColorText('WHEA APIC ID: ' + $apicIdString) Magenta Write-ColorText('WHEA MESSAGE: ' + $firstMessagEntry) Magenta $wheaMessageArray | ForEach-Object { @@ -9547,7 +9561,7 @@ function Test-StressTestProgrammIsRunning { Write-ColorText('WARNING: At Core ' + $coreString) Magenta Write-ColorText('WHEA TIMESTAMP: ' + $lastWheaError.TimeCreated.ToString()) Magenta Write-ColorText('WHEA EVENT ID: ' + $lastWheaError.Id) Magenta - Write-ColorText('WHEA APIC ID: ' + $apicIdFromWheaMessage) Magenta + Write-ColorText('WHEA APIC ID: ' + $apicIdString) Magenta Write-ColorText('WHEA MESSAGE: ' + $firstMessagEntry) Magenta $wheaMessageArray | ForEach-Object { @@ -9561,8 +9575,8 @@ function Test-StressTestProgrammIsRunning { $errorString = 'Timestamp: ' + $lastWheaError.TimeCreated.ToString() $errorString += [Environment]::NewLine + 'WHEA Record Id: ' + $lastWheaError.RecordId.ToString() $errorString += [Environment]::NewLine + 'WHEA Event Id: ' + $lastWheaError.Id.ToString() - $errorString += [Environment]::NewLine + 'APIC ID: ' + $apicIdFromWheaMessage - $errorString += [Environment]::NewLine + 'APIC ID converted to Core: ' + $coreFromWheaMessage + $errorString += [Environment]::NewLine + 'APIC ID: ' + $apicIdString + $errorString += [Environment]::NewLine + 'APIC ID converted to Core: ' + $coreIdString $errorString += [Environment]::NewLine + 'CoreCycler was running on Core: ' + $coreNumber $errorString += [Environment]::NewLine + [Environment]::NewLine + 'Message: ' + $lastWheaError.Message @@ -10008,10 +10022,10 @@ function Test-StressTestProgrammIsRunning { # A WHEA Error if ($errorType -eq 'WHEAERROR') { - Write-ColorText('ERROR: At Core ' + $coreString + ' (APIC ID ' + $apicIdFromWheaMessage + ' = Core ' + $coreFromWheaMessage + ')') Magenta + Write-ColorText('ERROR: At Core ' + $coreString + ' (APIC ID ' + $apicIdString + ' = Core ' + $coreIdString + ')') Magenta Write-ColorText('WHEA TIMESTAMP: ' + $lastWheaError.TimeCreated.ToString()) Magenta Write-ColorText('WHEA EVENT ID: ' + $lastWheaError.Id) Magenta - Write-ColorText('WHEA APIC ID: ' + $apicIdFromWheaMessage) Magenta + Write-ColorText('WHEA APIC ID: ' + $apicIdString) Magenta } @@ -11565,6 +11579,15 @@ function Convert-WheaMessageToCoreId { # The apicId represents the logical core, i.e. the virtual CPU $apicId = Convert-WheaMessageToApicId $wheaErrorEntry + # Some WHEA entries don't contain a processor reference at all (e.g. a corrected memory error), for those the + # APIC ID cannot be determined and neither can the core + # Note that a hash table lookup with a key that doesn't exist returns $null, which would be cast to core 0 here, + # so we need to check this explicitly + if ($apicId -lt 0 -or !$coresInfo['apicIdToCore'].ContainsKey($apicId)) { + Write-DebugText('The APIC ID ' + $apicId + ' could not be resolved to a core') + return -1 + } + # Convert this to a physical core $coreId = [Int] $coresInfo['apicIdToCore'][$apicId] From 906a92a508223a17ee97c360ae1c97ab6dcb0d81 Mon Sep 17 00:00:00 2001 From: Kristoffer Konstad Hansen Date: Tue, 28 Jul 2026 16:04:54 +0200 Subject: [PATCH 3/4] Automatic Test Mode: add repeatCoreUntilConfirmed to stay on a core until it has a final value Until now a core collected one confirmation pass per iteration, so with the default passesToConfirmCoreValue = 3 the whole test order had to be cycled through three times before the first core could be confirmed. On a 16 core processor that means the run has no final value for any core for a long time, and aborting it early leaves you with nothing. The new repeatCoreUntilConfirmed setting (default 1) re-inserts a core at the front of the test order after a successful test run, the same way repeatCoreOnError already does after an error, so the core is tested again right away. A core is therefore finished before the next one in line is started, and its value is written to the results file at that point. Setting it to 0 restores the previous behavior. The order in which the cores are selected is unchanged, it still comes from coreTestOrder. The loop index is decremented together with the re-insertion, so the number of test slots per iteration is unaffected and the persisted remaining core order stays correct for a resume. For Intel there is only a single voltage offset for all of the cores, so a core that is confirmed early keeps the value it was confirmed with even if a later core makes the shared value less aggressive. The Intel example config therefore sets this to 0 and the setting comment explains why. Co-Authored-By: Claude Opus 5 --- configs/Intel.AutomaticTestMode.yCruncher.ini | 5 ++ configs/Ryzen.AutomaticTestMode.Start.ini | 5 ++ configs/default.config.ini | 19 +++++++ readme.txt | 4 ++ script-corecycler.ps1 | 52 +++++++++++++++++-- 5 files changed, 81 insertions(+), 4 deletions(-) diff --git a/configs/Intel.AutomaticTestMode.yCruncher.ini b/configs/Intel.AutomaticTestMode.yCruncher.ini index 7abb5ea..e0696e9 100644 --- a/configs/Intel.AutomaticTestMode.yCruncher.ini +++ b/configs/Intel.AutomaticTestMode.yCruncher.ini @@ -26,6 +26,11 @@ incrementBy = 10 # Adjust the voltage offset by 10mv after an error # resets this counter for every core passesToConfirmCoreValue = 3 +# Cycle through all of the cores instead of staying on one core until it is confirmed +# As there is only one voltage offset for all of the cores, this lets all of them collect their test runs at +# the same value, instead of confirming the first cores at a value that a later core then adjusts again +repeatCoreUntilConfirmed = 0 + # The test runs until every core has found a good value, the "maxIterations" setting is not used # The found values are permanently stored in the "*_automode-results.txt" file in the logs directory diff --git a/configs/Ryzen.AutomaticTestMode.Start.ini b/configs/Ryzen.AutomaticTestMode.Start.ini index b5eaf4b..06b29b4 100644 --- a/configs/Ryzen.AutomaticTestMode.Start.ini +++ b/configs/Ryzen.AutomaticTestMode.Start.ini @@ -41,6 +41,11 @@ incrementBy = 1 # Adjust the Curve Optimizer value by 1 after an err passesToConfirmCoreValue = 3 +# Stay on a core until it has a final value, so that one core is finished after the other +# Set this to 0 to test each core once per iteration and cycle through the whole test order repeatedly instead +repeatCoreUntilConfirmed = 1 + + # If you already know the good value for some of the cores, you can enter them here # These cores will then not be tested at all # You can find a ready to use line at the end of the "*_automode-results.txt" file of a previous run diff --git a/configs/default.config.ini b/configs/default.config.ini index 9023516..cd6b1d7 100644 --- a/configs/default.config.ini +++ b/configs/default.config.ini @@ -634,6 +634,25 @@ incrementBy = Default passesToConfirmCoreValue = 3 +# Stay on a core until it has a final value, instead of continuing with the next core after each test run +# With this enabled, a core is tested over and over again until it has collected its "passesToConfirmCoreValue" +# error free test runs, or until it has reached the "maxValue" setting. Only then the next core in line is selected +# This finishes one core after the other, so you already have final values for the cores that have been tested if +# you abort the run early +# If you set this to 0, each core is tested once per iteration and the test order is cycled through repeatedly, which +# spreads the test runs for a core over a longer period of time +# +# Note: This does not change the order in which the cores are selected, see the "coreTestOrder" setting for that +# Note: For the behavior after an error, see the "repeatCoreOnError" setting below +# Note: For Intel up to 14th gen there is only one voltage offset for all of the cores. A core that is confirmed +# early therefore keeps the value it was confirmed with, even if a later core makes the shared value less +# aggressive. Setting this to 0 is the better choice there, as it lets all of the cores collect their test +# runs at the same value +# +# Default: 1 +repeatCoreUntilConfirmed = 1 + + # Set only the currently tested core to the selected Curve Optimizer / voltage offset value # All the other cores will be set to 0, resp. the value from "voltageValueForNotTestedCores", # or the determined maximum value if it's higher than any of those diff --git a/readme.txt b/readme.txt index 1fdf289..f35c32a 100644 --- a/readme.txt +++ b/readme.txt @@ -90,6 +90,10 @@ How it works: It will not be tested again either, and it will be listed as "could not be stabilized" in the summary. - The test keeps running until every core has either found a good value or has reached the "maxValue" setting. The "maxIterations" setting from the [General] section is NOT used in the Automatic Test Mode. +- By default the script stays on a core until that core has a final value ("repeatCoreUntilConfirmed"), so one core + is finished after the other. If you set this to 0, each core is tested once per iteration and the whole test order + is cycled through repeatedly instead, which spreads the test runs for a core over a longer period of time. + Either way, the order in which the cores are selected is the one from the "coreTestOrder" setting. The results: Every found value is immediately written to a "*_automode-results.txt" file in the /logs/ directory, next to the diff --git a/script-corecycler.ps1 b/script-corecycler.ps1 index c55804c..8e263be 100644 --- a/script-corecycler.ps1 +++ b/script-corecycler.ps1 @@ -191,6 +191,7 @@ $autoModeResultsFileFullPath = '' $autoModeResultsFooterWritten = $false $coreStates = @{} $passesToConfirmCoreValue = 3 +$repeatCoreUntilConfirmed = $true $maxTestsPerCoreSanityLimit = 0 $knownGoodValues = @{} $applyConfirmedValuesForNotTestedCores = $false @@ -866,6 +867,25 @@ incrementBy = Default passesToConfirmCoreValue = 3 +# Stay on a core until it has a final value, instead of continuing with the next core after each test run +# With this enabled, a core is tested over and over again until it has collected its "passesToConfirmCoreValue" +# error free test runs, or until it has reached the "maxValue" setting. Only then the next core in line is selected +# This finishes one core after the other, so you already have final values for the cores that have been tested if +# you abort the run early +# If you set this to 0, each core is tested once per iteration and the test order is cycled through repeatedly, which +# spreads the test runs for a core over a longer period of time +# +# Note: This does not change the order in which the cores are selected, see the "coreTestOrder" setting for that +# Note: For the behavior after an error, see the "repeatCoreOnError" setting below +# Note: For Intel up to 14th gen there is only one voltage offset for all of the cores. A core that is confirmed +# early therefore keeps the value it was confirmed with, even if a later core makes the shared value less +# aggressive. Setting this to 0 is the better choice there, as it lets all of the cores collect their test +# runs at the same value +# +# Default: 1 +repeatCoreUntilConfirmed = 1 + + # Set only the currently tested core to the selected Curve Optimizer / voltage offset value # All the other cores will be set to 0, resp. the value from "voltageValueForNotTestedCores", # or the determined maximum value if it's higher than any of those @@ -4997,6 +5017,7 @@ function Initialize-AutoModeResultsFile { $settingsString += ' | maxValue = ' + $settings['AutomaticTestMode']['maxValue'] $settingsString += ' | incrementBy = ' + $settings['AutomaticTestMode']['incrementBy'] $settingsString += ' | passesToConfirmCoreValue = ' + $passesToConfirmCoreValue + $settingsString += ' | repeatCoreUntilConfirmed = ' + $(if ($repeatCoreUntilConfirmed) { '1' } else { '0' }) Write-AutoModeResultEntry -text ('CoreCycler Automatic Test Mode - ' + $autoModeDescription + ' results') -NoLogEntry Write-AutoModeResultEntry -text ('Started: ' + (Get-Date -Format 'yyyy-MM-dd HH:mm:ss')) -NoLogEntry @@ -5305,12 +5326,18 @@ function Set-CoreState { If enough consecutive passes have been collected, the value for this core is considered confirmed .PARAMETER coreNumber [Int] The core that has completed a test run +.PARAMETER coreTestOrderArray + [System.Collections.ArrayList] The array of the cores being tested. We're modifying this if repeatCoreUntilConfirmed is set +.PARAMETER coreIndex + [Ref] The current core index. We're modifying this if repeatCoreUntilConfirmed is set .OUTPUTS [Void] #> function Add-CorePass { param( - [Parameter(Mandatory=$true)] [Int] $coreNumber + [Parameter(Mandatory=$true)] [Int] $coreNumber, + [Parameter(Mandatory=$false)][AllowEmptyCollection()] [System.Collections.ArrayList] $coreTestOrderArray, + [Parameter(Mandatory=$false)] $coreIndex ) if (!$useAutomaticTestMode -or !$coreStates.ContainsKey($coreNumber)) { @@ -5335,6 +5362,21 @@ function Add-CorePass { Write-ColorText(' Core ' + $coreNumber + ': ' + $thisCoreState['passes'] + '/' + $passesToConfirmCoreValue + ' confirmation passes at a ' + $autoModeDescription + ' value of ' + $thisValue) Cyan + # Stay on this core until it has a final value, instead of continuing with the next core in line + # This works the same way as the repeatCoreOnError setting: the core has already been removed from the test order + # array before the test started, so we put it back to the front and undo the increment of the loop index + if ($repeatCoreUntilConfirmed -and $null -ne $coreIndex) { + Write-DebugText('Core test order array before: ' + $coreTestOrderArray) + + [Void] $coreTestOrderArray.Insert(0, $coreNumber) + + Write-DebugText('Core test order array after: ' + $coreTestOrderArray) + + # This sets the index to the one before this core, so it should fall on the same core again when we continue the loop + # This variable was passed as a reference, so we need to access the .Value property + $coreIndex.Value-- + } + # No -coreNumber here, a completed pass must not change the core that is currently being tested Save-AutoModeState } @@ -6561,6 +6603,7 @@ function Initialize-AutomaticTestMode { # How many test runs in a row without an error a core needs to be considered "good" $Script:passesToConfirmCoreValue = [Math]::Max(1, [Int] $settings.AutomaticTestMode.passesToConfirmCoreValue) + $Script:repeatCoreUntilConfirmed = ($settings.AutomaticTestMode.repeatCoreUntilConfirmed -gt 0) $Script:applyConfirmedValuesForNotTestedCores = ($settings.AutomaticTestMode.applyConfirmedValuesForNotTestedCores -gt 0) @@ -14369,6 +14412,7 @@ try { } Write-SettingIntroText -Text 'Passes to confirm a value' -Setting ($passesToConfirmCoreValue) + Write-SettingIntroText -Text 'Stay on a core until it is confirmed' -Setting ($(if ($repeatCoreUntilConfirmed) { 'ENABLED' } else { 'DISABLED' })) if ($knownGoodValues.Count -gt 0) { Write-SettingIntroText -Text 'Known good values (not tested)' -Setting (Get-ConfirmedValuesString -OnlyFromConfig) @@ -15765,7 +15809,7 @@ try { # This core has completed a test run without an error if ($useAutomaticTestMode) { - Add-CorePass $actualCoreNumber + Add-CorePass -coreNumber $actualCoreNumber -coreTestOrderArray $coreTestOrderArray -coreIndex ([Ref] $coreIndex) } continue LoopCoreRunner @@ -16003,7 +16047,7 @@ try { # This core has completed a test run without an error if ($useAutomaticTestMode) { - Add-CorePass $actualCoreNumber + Add-CorePass -coreNumber $actualCoreNumber -coreTestOrderArray $coreTestOrderArray -coreIndex ([Ref] $coreIndex) } continue LoopCoreRunner @@ -16105,7 +16149,7 @@ try { # This core has completed a test run without an error if ($useAutomaticTestMode) { - Add-CorePass $actualCoreNumber + Add-CorePass -coreNumber $actualCoreNumber -coreTestOrderArray $coreTestOrderArray -coreIndex ([Ref] $coreIndex) } } # End: :LoopCoreRunner for ($coreIndex = 0; $coreIndex -lt $numAvailableCores; $coreIndex++) From b2073452bba0c826f836f3dc0f6216c9ae0a10c4 Mon Sep 17 00:00:00 2001 From: Kristoffer Konstad Hansen Date: Tue, 28 Jul 2026 20:07:59 +0200 Subject: [PATCH 4/4] Fix the crash on resume: Split-Path has no parameter set with both -LiteralPath and -Leaf Split-Path's LiteralPathSet consists of -LiteralPath, -Resolve and -Credential only. Combining -LiteralPath with -Leaf therefore cannot be bound to any parameter set and throws Parameter set cannot be resolved using the specified named parameters Both call sites are only reached when resuming after a crash, which is why this was not noticed earlier: - Initialize-AutoModeResultsFile takes the else branch only when the results file path was restored from the .automode file. It is not inside a try/catch, so this aborted the whole run with a fatal error on every single resume, which also removed the .automode file. - The log file re-use in Import-Settings is inside a try/catch, so it only lost the ability to continue writing to the log file of the interrupted run and silently started a new one. Both now use [System.IO.Path]::GetFileName(), which is a pure string operation with no parameter sets and no wildcard handling, so it also works for paths containing characters that Split-Path -Path would interpret as wildcards. Co-Authored-By: Claude Opus 5 --- script-corecycler.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/script-corecycler.ps1 b/script-corecycler.ps1 index 8e263be..46d7662 100644 --- a/script-corecycler.ps1 +++ b/script-corecycler.ps1 @@ -4999,7 +4999,7 @@ function Initialize-AutoModeResultsFile { $Script:autoModeResultsFileFullPath = $logFilePathAbsolute + $autoModeResultsFileName } else { - $Script:autoModeResultsFileName = Split-Path -LiteralPath $autoModeResultsFileFullPath -Leaf + $Script:autoModeResultsFileName = [System.IO.Path]::GetFileName($autoModeResultsFileFullPath) } Write-DebugText('The Automatic Test Mode results file: ' + $autoModeResultsFileFullPath) @@ -6270,7 +6270,7 @@ function Get-Settings { Write-DebugText('logFileCoreCycler: ' + $autoModeInfo['logFileCoreCycler']) #Write-DebugText('logFileStressTest: ' + $autoModeInfo['logFileStressTest']) - $Script:logFileName = Split-Path -LiteralPath $autoModeInfo['logFileCoreCycler'] -Leaf + $Script:logFileName = [System.IO.Path]::GetFileName($autoModeInfo['logFileCoreCycler']) $Script:logFileFullPath = $autoModeInfo['logFileCoreCycler'] $Script:canUseLogFile = $true }