From e2ad7e7a027c5298ff3893d5e55163dc2b9fbe22 Mon Sep 17 00:00:00 2001 From: Al Date: Tue, 7 Jul 2026 00:06:51 +0200 Subject: [PATCH] CPU Multithreading - Single/Multithread support in Options --- docs/multicore.md | 63 ++ docs/rundown.md | 2 + runtime/Path of Building.exe | Bin 0 -> 410624 bytes spec/System/TestParallelTasks_spec.lua | 228 ++++ src/CalcWorker.lua | 429 ++++++++ src/Classes/CalcsTab.lua | 303 +++++- src/Classes/CompareTab.lua | 1087 ++++++++++--------- src/Classes/ItemDBControl.lua | 136 ++- src/Classes/NotableDBControl.lua | 143 ++- src/Launch.lua | 23 + src/Modules/Build.lua | 8 + src/Modules/DataLegionLookUpTableHelper.lua | 28 +- src/Modules/Main.lua | 43 +- src/Modules/ParallelRunner.lua | 361 ++++++ src/Modules/PowerCalcTasks.lua | 341 ++++++ src/parallel_log.txt | 103 ++ src/probe_report.txt | 37 + 17 files changed, 2772 insertions(+), 563 deletions(-) create mode 100644 docs/multicore.md create mode 100644 runtime/Path of Building.exe create mode 100644 spec/System/TestParallelTasks_spec.lua create mode 100644 src/CalcWorker.lua create mode 100644 src/Modules/ParallelRunner.lua create mode 100644 src/Modules/PowerCalcTasks.lua create mode 100644 src/parallel_log.txt create mode 100644 src/probe_report.txt diff --git a/docs/multicore.md b/docs/multicore.md new file mode 100644 index 00000000000..eb76ff3ecd3 --- /dev/null +++ b/docs/multicore.md @@ -0,0 +1,63 @@ +# Multi-core calculation support — dev handoff notes + +## Changelog (PR summary) + +**Adds multi-core support for the four "iterate candidates, rank outcomes" features, with a user toggle (default: multi-core) and automatic single-core fallback.** + +Parallelized features: +- Passive tree node power / heat map / "Show Power Report" (`CalcsTab:PowerBuilder`) +- Compare tab power report (`CompareTab`) +- Anoint notable stat-sorting (`NotableDBControl`) +- Item DB stat-sorting (`ItemDBControl`) + +### Architecture + +Workers are one-shot isolated Lua VMs started via the engine's existing `LaunchSubScript` primitive (no engine changes). Each worker bootstraps a headless PoB (HeadlessWrapper pattern), reconstructs the build from an in-memory `SaveDB` XML snapshot, computes its batch using **the same per-candidate functions the single-core path runs**, and returns one JSON payload (dkjson). The main thread partitions candidates, aggregates progress via subList calls, validates results, and merges by stable IDs. + +### New files +- `src/Modules/ParallelRunner.lua` — worker pool / job manager: launch, cancel, progress aggregation, baseline validation, per-session sticky fallback, dev-mode job log (`src/parallel_log.txt`) +- `src/CalcWorker.lua` — worker script (read via `io.open`, passed as scriptText like `UpdateCheck.lua`): hardened headless stubs, script-path-relative `io.open` resolution (worker threads don't inherit a guaranteed cwd), env probe mode +- `src/Modules/PowerCalcTasks.lua` — shared per-candidate compute logic + JSON number sentinels + baseline extraction, loaded by both main thread and workers +- `spec/System/TestParallelTasks_spec.lua` — equivalence tests: single-core vs worker entry points in-process, through a JSON round trip, for all four features; merge staleness tests + +### Modified files +- `src/Launch.lua` — `OnSubError` now notifies CUSTOM subscript callbacks (`(nil, errMsg)` — audited compatible with all existing users); nil-guards in `OnSubError`/`OnSubFinished` for aborted subscripts; dev-mode F7 = worker environment probe (report written to `src/probe_report.txt`) +- `src/Classes/CalcsTab.lua` — `PowerBuilder(filter)` optional candidate filter (nil = byte-identical legacy behavior); `EnumeratePowerCandidates`/`BuildPowerBatches` (modKey-grouped, weight-balanced batches carrying authoritative `node.path`/`node.depends` id lists); dual-path `BuildPower` with duplicate-launch guard; `LaunchParallelPowerBuild`/`MergeParallelPower` (staleness via `outputRevision`) +- `src/Classes/CompareTab.lua` — `ComparePowerBuilder` split into enumerate / `ComputeComparePowerCandidate` / format-row so both paths share candidate computation; dual-path report runner +- `src/Classes/NotableDBControl.lua`, `src/Classes/ItemDBControl.lua` — `ListBuilder` split into filter / measure / finish; dual-path `Draw`; candidates keyed by node id / item name +- `src/Modules/Main.lua` — `computationMode` ("MULTI"/"SINGLE", default MULTI) and `workerCount` (0 = auto) settings, persisted in `Settings.xml`; Options popup UI; loads `ParallelRunner`/`PowerCalcTasks` as globals +- `src/Modules/Build.lua` — cancels outstanding worker jobs on shutdown +- `src/Modules/DataLegionLookUpTableHelper.lua` — **standalone bugfix**: `loadJewelFile` could cache empty jewel data to `.bin` when `Inflate` is stubbed (headless/busted), silently corrupting timeless jewel stats for the full app afterwards. Now rejects empty data, reads an existing `.bin` directly when `NewFileSearch` is unavailable, and never writes the shared cache from headless contexts + +### Correctness guarantees +- Workers and single-core run identical code; results cross the thread boundary as JSON with `inf`/NaN sentinels +- Every worker returns a baseline stat vector recomputed from its reconstructed build; any relative deviation > 1e-3 from the main thread's baseline fails the whole job → automatic single-core fallback (sticky for the session, cleared by toggling the option or a successful F7 probe) +- Main thread sends each candidate's live `path`/`depends` node ids: a spec rebuilt from XML resolves equal-length shortest-path ties in different `pairs()` order, which would otherwise change `pathPower` vs what the UI displays +- Results merge only if `build.outputRevision` is unchanged since the snapshot; any build change mid-job cancels/discards and re-runs +- Worker errors surface once via `OnSubError` → fallback; workers never write shared caches +- Headless/CI: `HeadlessWrapper.lua` stubs `LaunchSubScript` → everything transparently uses the single-core path; no platform checks anywhere (pure feature detection) + +### Verified +- `spec/System/TestParallelTasks_spec.lua`: 6/6 (all four features numerically identical through the worker entry points + JSON round trip) +- Full existing spec suite: zero regressions vs pristine master baseline (same harness, failure sets identical) +- Cross-process fresh-VM E2E: 781/781 nodes bit-identical on a 3.13 test build; pass on a live 3.28 build with a Brutal Restraint timeless jewel +- Real engine: worker baseline probe exact match on all stats; heat map on a level-95 build computes 4,348 candidates in ~15 s (8 workers) vs ~48 s single-core +- Engine facts established via probe: subscript VMs receive funcList injections (incl. `GetUserPath`) but not `NewFileSearch`/`Inflate`/`Deflate`; worker-thread cwd is launch-dependent (both handled in `CalcWorker.lua`) + +### Performance (measured, 5950X SMT-off, level-95 build, full-depth heat map) +- Single-core: ~48 s (one core busy) +- Multi-core, 8 workers (auto default): ~15–27 s depending on system load +- Fixed overhead per job: ~5–7 s worker bootstrap (one-shot VMs must load all game data + build) + ~1–2 s snapshot/merge — this is the Amdahl floor; raise `worker threads` to physical cores − 1 to approach it + +--- + +## Future speed projects (ordered by expected impact) + +1. **Persistent warm workers (engine change — removes the ~7 s floor).** `LaunchSubScript` VMs are one-shot; every job pays a full bootstrap. A persistent worker/channel primitive in SimpleGraphic (send job → receive result → VM stays alive) would drop heat-map latency to pure compute (~3–6 s at 15 workers). Largest win, requires C++ runtime work upstream. +2. **Result caching across tree toggles.** Allocating then un-allocating a node currently recomputes everything twice. Caching the last few power-result sets keyed by a spec/state hash makes "undo a change" instant. Cheap, and directly targets the most common interaction. +3. **Trim worker bootstrap.** Profile `timing.initMs` vs `bootstrapMs` (already reported by workers): skip unique/rare item DB loading outside itemdb mode, skip tree versions the build doesn't use, lazy-load legion/tattoo data unless present, and cache Data modules as precompiled bytecode (`string.dump`). Realistic target: 7 s → 3–4 s. +4. **Wave scheduling for stragglers.** The job ends when the slowest worker finishes; per-candidate cost varies (masteries, long paths). Splitting into ~2× more batches than workers and launching a second wave as results return evens out the tail without engine changes. +5. **Stat-scoped calculation (benefits single-core too).** Each candidate runs a full offence+defence pipeline even when the heat map only needs one stat (e.g. Life). A calculation mode that skips unneeded sections could cut per-candidate cost several-fold — invasive, touches `calcs.perform`. +6. **Parallelize the Timeless Jewel search.** "Find Timeless Jewel" brute-forces thousands of seeds through the same kind of loop and is a notorious multi-minute wait — it fits the existing ParallelRunner/batch pattern well and may be the single most user-visible follow-up. +7. **Progressive merge.** Color the tree per-worker as payloads arrive instead of waiting for the last one — no wall-time change, large perceived-latency change. +8. **Auto worker count tuning.** Auto currently clamps at 8 for memory safety; scale the cap with detected physical cores and available RAM instead (e.g. `min(cores − 1, RAM_GB / 1.5, 16)`). diff --git a/docs/rundown.md b/docs/rundown.md index 03fde7149dd..5cb93603bad 100644 --- a/docs/rundown.md +++ b/docs/rundown.md @@ -135,6 +135,8 @@ Patch notes written in plain text. * **CONTRIBUTING.md** Contribution guides. +* **CalcWorker.lua** + Background calculation worker script, executed in isolated Lua VMs on worker threads via `LaunchSubScript` (see `Modules/ParallelRunner.lua`). Bootstraps a headless copy of the program (like `HeadlessWrapper.lua`), reconstructs the build from an XML snapshot, computes a batch of candidates via `Modules/PowerCalcTasks.lua`, and returns the results as JSON. * **GameVersions.lua** Contains global variables to identify and convert outdated builds. Also contains global table of passive skill tree versions used to upgrade to newer skill tree versions. * **HeadlessWrapper.lua** diff --git a/runtime/Path of Building.exe b/runtime/Path of Building.exe new file mode 100644 index 0000000000000000000000000000000000000000..5e7ecf7b732f500823014726fe1886b925ea65c7 GIT binary patch literal 410624 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 z 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%0ALQS$-#q;VdBdZM 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 Q(83jLzzcTY?z#m2AM^GzfdBvi literal 0 HcmV?d00001 diff --git a/spec/System/TestParallelTasks_spec.lua b/spec/System/TestParallelTasks_spec.lua new file mode 100644 index 00000000000..a3378198973 --- /dev/null +++ b/spec/System/TestParallelTasks_spec.lua @@ -0,0 +1,228 @@ +-- Tests for the multi-core calculation support (Modules/ParallelRunner, +-- Modules/PowerCalcTasks, CalcWorker.lua). +-- +-- Real worker threads can't run headless (LaunchSubScript is stubbed), so +-- these tests run the worker entry points (computeBatch) in-process on the +-- same build, round-trip the payloads through JSON exactly as the thread +-- boundary would, merge them, and require results identical to the +-- single-core code path. +local dkjson = require "dkjson" + +local function jsonRoundTrip(payload) + return dkjson.decode(dkjson.encode(payload)) +end + +local function numEq(a, b) + if a == nil and b == nil then + return true + end + if type(a) ~= "number" or type(b) ~= "number" then + return a == b + end + if a ~= a and b ~= b then + return true -- both NaN + end + return math.abs(a - b) <= math.max(1e-9, 1e-9 * math.max(math.abs(a), math.abs(b))) +end + +describe("TestParallelTasks", function() + before_each(function() + newBuild() + end) + + it("number sentinels survive the JSON boundary", function() + local enc = PowerCalcTasks.encodeNumber + local dec = PowerCalcTasks.decodeNumber + assert.are.equals(5.25, dec(jsonRoundTrip({ v = enc(5.25) }).v)) + assert.are.equals(math.huge, dec(jsonRoundTrip({ v = enc(math.huge) }).v)) + assert.are.equals(-math.huge, dec(jsonRoundTrip({ v = enc(-math.huge) }).v)) + assert.are.equals(0, dec(jsonRoundTrip({ v = enc(0/0) }).v)) -- NaN guarded to 0 + assert.are.equals(nil, dec(nil)) + end) + + it("nodePower worker batches merge to the same result as single-core PowerBuilder", function() + local calcsTab = build.calcsTab + calcsTab.powerStat = data.powerStatList[4] -- Combined DPS + calcsTab.nodePowerMaxDepth = 4 + + -- Single-core reference (direct call; outside a coroutine it never yields) + calcsTab:PowerBuilder() + local refPower = { } + for nodeId, node in pairs(build.spec.nodes) do + refPower[nodeId] = { + singleStat = node.power and node.power.singleStat, + pathPower = node.power and node.power.pathPower, + } + end + local refPowerMax = copyTable(calcsTab.powerMax) + + -- Parallel simulation: enumerate, batch exactly as production does + -- (including authoritative path data), compute, JSON round trip, merge + local cand = calcsTab:EnumeratePowerCandidates() + assert.True(cand.total > 0) + local batches = calcsTab:BuildPowerBatches(cand, 3) + local common = { powerStatIndex = 4, nodePowerMaxDepth = 4 } + local payloads = { } + for i, batch in ipairs(batches) do + payloads[i] = jsonRoundTrip(PowerCalcTasks.nodePower.computeBatch(build, common, batch, "", nil)) + -- Worker payloads carry a baseline for divergence detection + assert.is_table(payloads[i].baseline) + end + calcsTab:MergeParallelPower(payloads, build.outputRevision) + + for nodeId, ref in pairs(refPower) do + local node = build.spec.nodes[nodeId] + assert.True(numEq(ref.singleStat, node.power and node.power.singleStat), + "singleStat mismatch on node "..tostring(nodeId)) + assert.True(numEq(ref.pathPower, node.power and node.power.pathPower), + "pathPower mismatch on node "..tostring(nodeId)) + end + for key, value in pairs(refPowerMax) do + assert.True(numEq(value, calcsTab.powerMax[key]), "powerMax mismatch on "..key) + end + end) + + it("nodePower merge discards results from a stale build revision", function() + local calcsTab = build.calcsTab + calcsTab.powerBuildFlag = false + calcsTab:MergeParallelPower({ }, build.outputRevision - 1) + assert.True(calcsTab.powerBuildFlag) + end) + + it("notable measureOne matches the worker batch path", function() + local itemsTab = build.itemsTab + itemsTab.displayItem = new("Item", "Rarity: RARE\nTest Amulet\nJade Amulet\n+30 to Dexterity") + itemsTab.anointEnchantSlot = 1 + + -- Collect a handful of anointable notables + local nodeIds = { } + for nodeId, node in pairs(build.spec.tree.nodes) do + if node.recipe and #node.recipe >= 1 and node.modKey ~= "" then + table.insert(nodeIds, nodeId) + if #nodeIds >= 15 then + break + end + end + end + assert.True(#nodeIds > 0) + + -- Single-core reference via the shared measureOne + local sortDetail = PowerCalcTasks.findSortDetail("CombinedDPS") + local calcFunc = build.calcsTab:GetMiscCalculator() + local itemType = itemsTab.displayItem.base.type + local calcBase = calcFunc({ repSlotName = itemType, repItem = itemsTab:anointItem(nil) }) + local ref = { } + for _, nodeId in ipairs(nodeIds) do + ref[nodeId] = PowerCalcTasks.notable.measureOne(itemsTab, calcFunc, calcBase, sortDetail, itemType, build.spec.tree.nodes[nodeId]) + end + + -- Worker path + local payload = jsonRoundTrip(PowerCalcTasks.notable.computeBatch(build, + { sortMode = "CombinedDPS", anointEnchantSlot = 1 }, + { nodeIds = nodeIds }, + itemsTab.displayItem:BuildRaw(), nil)) + for _, nodeId in ipairs(nodeIds) do + local workerPower = PowerCalcTasks.decodeNumber(payload.results[tostring(nodeId)]) + assert.True(numEq(ref[nodeId], workerPower), "measuredPower mismatch on node "..tostring(nodeId)) + end + end) + + it("itemdb measureOne matches the worker batch path", function() + -- Item DBs load over frames in the background + local guard = 0 + while main.onFrameFuncs["LoadItems"] and guard < 1000 do + runCallback("OnFrame") + guard = guard + 1 + end + local names = { } + for name in pairs(main.uniqueDB.list) do + table.insert(names, name) + if #names >= 10 then + break + end + end + assert.True(#names > 0) + + local itemsTab = build.itemsTab + local sortDetail = PowerCalcTasks.findSortDetail("Life") + local calcFunc = build.calcsTab:GetMiscCalculator() + local ref = { } + for _, name in ipairs(names) do + ref[name] = PowerCalcTasks.itemdb.measureOne(itemsTab, calcFunc, sortDetail, sortDetail.sortMode, false, main.uniqueDB.list[name]) + end + + local payload = jsonRoundTrip(PowerCalcTasks.itemdb.computeBatch(build, + { sortMode = "Life", dbType = "UNIQUE" }, + { names = names }, "", nil)) + for _, name in ipairs(names) do + local workerPower = PowerCalcTasks.decodeNumber(payload.results[name]) + assert.True(numEq(ref[name], workerPower), "measuredPower mismatch on item "..name) + end + end) + + it("compare descriptors compute identically through the worker entry point", function() + local primaryFile = io.open("../spec/TestBuilds/3.13/OccVortex.xml", "r") + local compareFile = io.open("../spec/TestBuilds/3.13/Mirage Archer Toxic Rain.xml", "r") + assert.is_not_nil(primaryFile) + assert.is_not_nil(compareFile) + local primaryXml = primaryFile:read("*a") + primaryFile:close() + local compareXml = compareFile:read("*a") + compareFile:close() + + loadBuildFromXML(primaryXml, "Primary") + for i = 1, 5 do + runCallback("OnFrame") + end + local compareTab = build.compareTab + local compareEntry = new("CompareEntry", compareXml, "Comparison") + compareEntry.xmlText = compareXml + local powerStat = data.powerStatList[4] -- Combined DPS + local categories = { treeNodes = true, items = true, skillGems = true, supportGems = true, config = true } + + -- Single-core reference (run the real coroutine to completion) + local co = coroutine.create(function() + compareTab:ComparePowerBuilder(compareEntry, powerStat, categories) + end) + repeat + local res, errMsg = coroutine.resume(co) + assert.True(res, errMsg) + until coroutine.status(co) == "dead" + local refResults = compareTab.comparePowerResults + assert.True(#refResults > 0) + + -- Parallel simulation + local descriptors = compareTab:EnumerateComparePowerCandidates(compareEntry, categories) + for i, desc in ipairs(descriptors) do + desc.i = i + end + local calcFunc, calcBase = compareTab.calcs.getMiscCalculator(build) + local fmtCtx = compareTab:MakeComparePowerFormatContext(compareEntry, powerStat, calcBase) + local resultByIndex = { } + for _, slice in ipairs(ParallelRunner:PartitionList(descriptors, 3)) do + local payload = jsonRoundTrip(PowerCalcTasks.compare.computeBatch(build, + { powerStatIndex = 4 }, { descriptors = slice }, compareXml, nil)) + for _, res in ipairs(payload.results) do + resultByIndex[res.i] = res + end + end + local parResults = { } + for i, desc in ipairs(descriptors) do + local res = resultByIndex[i] + if res and res.ok and res.impact then + local row = compareTab:BuildComparePowerRow(desc, PowerCalcTasks.decodeNumber(res.impact), res.extra or { }, fmtCtx) + if row then + table.insert(parResults, row) + end + end + end + + assert.are.equals(#refResults, #parResults) + for i = 1, #refResults do + assert.are.equals(refResults[i].category, parResults[i].category) + assert.are.equals(refResults[i].name, parResults[i].name) + assert.True(numEq(refResults[i].impact, parResults[i].impact), "impact mismatch on row "..i) + assert.are.equals(refResults[i].combinedImpactStr, parResults[i].combinedImpactStr) + end + end) +end) diff --git a/src/CalcWorker.lua b/src/CalcWorker.lua new file mode 100644 index 00000000000..a11772e19b3 --- /dev/null +++ b/src/CalcWorker.lua @@ -0,0 +1,429 @@ +#@ +-- Path of Building +-- +-- Module: Calc Worker +-- Background calculation worker, launched via LaunchSubScript by Modules/ParallelRunner. +-- Runs in an isolated Lua VM on its own thread: bootstraps a headless copy of the +-- program (modelled on HeadlessWrapper.lua), reconstructs the build from an XML +-- snapshot, computes the requested batch of candidates, and returns a JSON string. +-- +-- Available in this VM: the Lua standard library, require() for bundled binary/lua +-- modules, plus engine functions injected via funcList (GetScriptPath etc.) and +-- main-thread callbacks via subList (ConPrintf, ParallelWorkerProgress). + +local jobId, workerIdx, scriptPath, workerMode, buildXML, auxText, specJSON = ... + +-- Keep any engine-injected functions before defining stubs +local engineScriptPath = GetScriptPath +local engineRuntimePath = GetRuntimePath +local engineUserPath = GetUserPath +local engineWorkDir = GetWorkDir +local engineConPrintf = ConPrintf +local engineNewFileSearch = NewFileSearch +local engineInflate = Inflate +local engineDeflate = Deflate + +-- Millisecond timer; on Windows os.clock() is wall time, which is all we need +-- for timing telemetry and the calc engine's internal pacing checks +local osClock = os.clock +local function nowMs() + return osClock() * 1000 +end + +-- JSON codec, needed both for the work spec and to report errors; loaded before +-- anything else so failures later on can still be serialized +local dkjson +do + local ok, mod = pcall(require, "dkjson") + if ok and mod then + dkjson = mod + else + local candidates = { } + if engineRuntimePath then + candidates[#candidates + 1] = engineRuntimePath().."/lua/dkjson.lua" + end + candidates[#candidates + 1] = scriptPath.."/../runtime/lua/dkjson.lua" + for _, path in ipairs(candidates) do + local func = loadfile(path) + if func then + dkjson = func() + break + end + end + end + if not dkjson then + error("CalcWorker: unable to load dkjson") + end +end + +------------------------------------------------------------------------------ +-- Headless environment stubs (hardened copy of HeadlessWrapper.lua) +------------------------------------------------------------------------------ + +-- Callbacks +local callbackTable = { } +local mainObject +function runCallback(name, ...) + if callbackTable[name] then + return callbackTable[name](...) + elseif mainObject and mainObject[name] then + return mainObject[name](mainObject, ...) + end +end +function SetCallback(name, func) + callbackTable[name] = func +end +function GetCallback(name) + return callbackTable[name] +end +function SetMainObject(obj) + mainObject = obj +end + +-- Image Handles +local imageHandleClass = { } +imageHandleClass.__index = imageHandleClass +function NewImageHandle() + return setmetatable({ }, imageHandleClass) +end +function imageHandleClass:Load(fileName, ...) + self.valid = true +end +function imageHandleClass:Unload() + self.valid = false +end +function imageHandleClass:IsValid() + return self.valid +end +function imageHandleClass:SetLoadingPriority(pri) end +function imageHandleClass:ImageSize() + return 1, 1 +end + +-- Rendering +function RenderInit(flag, ...) end +function GetScreenSize() + return 1920, 1080 +end +function GetScreenScale() + return 1 +end +function GetVirtualScreenSize() + return GetScreenSize() +end +function GetDPIScaleOverridePercent() + return 1 +end +function SetDPIScaleOverridePercent(scale) end +function SetClearColor(r, g, b, a) end +function SetDrawLayer(layer, subLayer) end +function SetViewport(x, y, width, height) end +function SetDrawColor(r, g, b, a) end +function DrawImage(imgHandle, left, top, width, height, tcLeft, tcTop, tcRight, tcBottom) end +function DrawImageQuad(imageHandle, x1, y1, x2, y2, x3, y3, x4, y4, s1, t1, s2, t2, s3, t3, s4, t4) end +function DrawString(left, top, align, height, font, text) end +function DrawStringWidth(height, font, text) + return 1 +end +function DrawStringCursorIndex(height, font, text, cursorX, cursorY) + return 0 +end +function StripEscapes(text) + return text:gsub("%^%d",""):gsub("%^x%x%x%x%x%x%x","") +end +function GetAsyncCount() + return 0 +end + +-- Search Handles; prefer the engine implementation if this VM has one. +-- The stub returning nil makes data loaders (e.g. timeless jewel LUTs) fall +-- back to reading .bin caches directly — they must NOT get fake handles +NewFileSearch = engineNewFileSearch or function() end + +-- General Functions +function SetWindowTitle(title) end +function GetCursorPos() + return 0, 0 +end +function SetCursorPos(x, y) end +function ShowCursor(doShow) end +function IsKeyDown(keyName) end +function Copy(text) end +function Paste() end +-- Prefer the engine implementations if this VM has them. The stubs return nil +-- (NOT "") so callers treat compression as unavailable instead of caching +-- empty data — an empty string here once poisoned the shared timeless jewel +-- .bin cache on disk +Deflate = engineDeflate or function(data) + return nil +end +Inflate = engineInflate or function(data) + return nil +end +function GetTime() + return nowMs() +end +-- Prefer real engine paths where they were injected; fall back to the path the +-- main thread passed as an argument +GetScriptPath = engineScriptPath or function() + return scriptPath +end +GetRuntimePath = engineRuntimePath or function() + return scriptPath.."/../runtime" +end +GetUserPath = engineUserPath or function() + return "" +end +GetWorkDir = engineWorkDir or function() + return "" +end +function GetCloudProvider(fullPath) + return nil, nil, nil +end +function MakeDir(path) end +function RemoveDir(path) end +function SetWorkDir(path) end +-- Workers may not spawn further workers +function LaunchSubScript(scriptText, funcList, subList, ...) end +function AbortSubScript(ssID) end +function IsSubScriptRunning(ssID) end +function LoadModule(fileName, ...) + if not fileName:match("%.lua") then + fileName = fileName .. ".lua" + end + local func, err = loadfile(fileName) + if not func then + -- The worker thread's working directory may not be the script path; + -- retry with an absolute path + func, err = loadfile(GetScriptPath().."/"..fileName) + end + if func then + return func(...) + else + error("LoadModule() error loading '"..fileName.."': "..err) + end +end +function PCall(func, ...) + local ret = { pcall(func, ...) } + if ret[1] then + table.remove(ret, 1) + return nil, unpack(ret) + else + return ret[2] + end +end +function PLoadModule(fileName, ...) + if not fileName:match("%.lua") then + fileName = fileName .. ".lua" + end + local func, err = loadfile(fileName) + if not func then + func, err = loadfile(GetScriptPath().."/"..fileName) + end + if func then + return PCall(func, ...) + else + error("PLoadModule() error loading '"..fileName.."': "..err) + end +end +-- ConPrintf is normally marshalled to the main thread via subList; keep a +-- local fallback in case it wasn't injected +ConPrintf = ConPrintf or function(fmt, ...) + print(string.format(fmt, ...)) +end +function ConPrintTable(tbl, noRecurse) end +function ConExecute(cmd) end +function ConClear() end +function SpawnProcess(cmdName, args) end +function OpenURL(url) end +function SetProfiling(isEnabled) end +function Restart() end +function Exit() end +function TakeScreenshot() end + +-- Launch.lua indexes these at load time +arg = arg or { } +jit = jit or { opt = { start = function() end } } + +local l_require = require +function require(name) + -- Don't hard-fail if lcurl isn't loadable in this VM; nothing that runs in + -- a worker actually performs HTTP requests + if name == "lcurl.safe" then + local ok, mod = pcall(l_require, name) + return ok and mod or nil + end + return l_require(name) +end + +-- Relative paths resolve against the process working directory, which is only +-- guaranteed to be the script path for the main thread. Resolve relative opens +-- against the script path first so file access behaves like the main thread +-- (e.g. "TreeData//tree.lua" in PassiveTree). +local l_ioOpen = io.open +local function isRelativePath(fileName) + return type(fileName) == "string" and not fileName:match("^%a:[/\\]") and not fileName:match("^[/\\]") +end +function io.open(fileName, ...) + if isRelativePath(fileName) then + local file = l_ioOpen(scriptPath.."/"..fileName, ...) + if file then + return file + end + end + return l_ioOpen(fileName, ...) +end + +------------------------------------------------------------------------------ +-- Worker body +------------------------------------------------------------------------------ + +local function canOpen(path) + local file = l_ioOpen(path, "rb") + if file then + file:close() + return true + end + return false +end + +-- Filled in as early as possible so error payloads can carry environment +-- diagnostics even when bootstrap fails +local envProbe + +local function checkForStartupError() + if mainObject and mainObject.promptMsg then + error("worker startup failed: "..tostring(mainObject.promptMsg)) + end +end + +local function runWorker() + local timing = { } + local startTime = nowMs() + envProbe = { + scriptPathArg = scriptPath, + workDir = engineWorkDir and engineWorkDir() or "?", + treeOpenRelative = canOpen("TreeData/3_28/tree.lua"), + treeOpenAbsolute = canOpen(scriptPath.."/TreeData/3_28/tree.lua"), + } + + -- Bootstrap the program + local launchDoFile, dofileErr = loadfile(scriptPath.."/Launch.lua") + if not launchDoFile then + error("unable to load Launch.lua: "..tostring(dofileErr)) + end + launchDoFile() + -- The worker must never kick off an update check + mainObject.CheckForUpdate = function() end + runCallback("OnInit") + checkForStartupError() + timing.initMs = nowMs() - startTime + + local spec = dkjson.decode(specJSON or "") or { } + local common = spec.common or { } + local batch = spec.batch or { } + + -- Reconstruct the build from the XML snapshot. Init has queued a mode change + -- to an empty build; replacing it before the first frame means the worker + -- never wastes time building that placeholder (or loading its tree version) + local build + if buildXML and #buildXML > 0 then + mainObject.main:SetMode("BUILD", false, "CalcWorker", buildXML) + runCallback("OnFrame") + checkForStartupError() + build = mainObject.main.modes["BUILD"] + -- Let deferred frame work settle (initial calc pass, item DB loading for + -- modes that need it); each frame is cheap once the flags have cleared + local framesLeft = 1000 + while framesLeft > 0 do + local needMoreFrames = build.buildFlag or build.modFlag + if workerMode == "itemdb" and mainObject.main.onFrameFuncs["LoadItems"] then + needMoreFrames = true + end + if not needMoreFrames then + break + end + runCallback("OnFrame") + checkForStartupError() + framesLeft = framesLeft - 1 + end + else + runCallback("OnFrame") -- Need at least one frame for everything to initialise + checkForStartupError() + end + timing.bootstrapMs = nowMs() - startTime + + local result + if workerMode == "probe" then + -- Environment probe: report facts about this VM so assumptions about the + -- worker environment can be verified at runtime instead of guessed at + local probe = { + luaVersion = _VERSION, + jitVersion = (rawget(_G, "jit") and jit.version) or "none", + hasLoadfile = type(loadfile) == "function", + hasIo = type(io) == "table", + packagePath = package and package.path or "?", + injectedGetScriptPath = engineScriptPath ~= nil, + injectedGetRuntimePath = engineRuntimePath ~= nil, + injectedGetUserPath = engineUserPath ~= nil, + injectedGetWorkDir = engineWorkDir ~= nil, + injectedConPrintf = engineConPrintf ~= nil, + injectedNewFileSearch = engineNewFileSearch ~= nil, + injectedInflate = engineInflate ~= nil, + buildXMLBytes = buildXML and #buildXML or 0, + specJSONBytes = specJSON and #specJSON or 0, + buildLoaded = build ~= nil, + buildName = build and build.buildName or "none", + } + for key, value in pairs(envProbe or { }) do + probe[key] = value + end + result = { probe = probe } + if build then + local calcStart = nowMs() + local tasks = LoadModule("Modules/PowerCalcTasks") + local calcFunc, calcBase = build.calcsTab:GetMiscCalculator() + result.baseline = tasks.extractBaseline(calcBase, nil, false) + -- Time a couple of representative calcFunc calls + calcFunc({ }, false) + probe.singleCalcMs = nowMs() - calcStart + timing.calcMs = nowMs() - calcStart + end + if ParallelWorkerProgress then + ParallelWorkerProgress(jobId, workerIdx, 1, 1) + end + else + if not build then + error("no build XML provided for mode "..tostring(workerMode)) + end + local tasks = LoadModule("Modules/PowerCalcTasks") + local task = tasks[workerMode] + if not task then + error("unknown worker mode: "..tostring(workerMode)) + end + local calcStart = nowMs() + local progressFunc + if ParallelWorkerProgress then + progressFunc = function(done, total) + ParallelWorkerProgress(jobId, workerIdx, done, total) + end + end + result = task.computeBatch(build, common, batch, auxText, progressFunc) + timing.calcMs = nowMs() - calcStart + end + + timing.totalMs = nowMs() - startTime + result.timing = timing + return dkjson.encode(result) +end + +local ok, resultOrErr = xpcall(runWorker, function(err) + return tostring(err).."\n"..debug.traceback() +end) +if ok then + return resultOrErr +end +-- Return the failure as a structured payload so the main thread gets a clean +-- error message (an uncaught error would also work via OnSubError, but with +-- less context); include the environment diagnostics for probe reports +return dkjson.encode({ error = resultOrErr, probe = envProbe }) diff --git a/src/Classes/CalcsTab.lua b/src/Classes/CalcsTab.lua index 690103ba096..3088f119564 100644 --- a/src/Classes/CalcsTab.lua +++ b/src/Classes/CalcsTab.lua @@ -6,6 +6,7 @@ local pairs = pairs local ipairs = ipairs local t_insert = table.insert +local t_remove = table.remove local m_max = math.max local m_floor = math.floor @@ -450,12 +451,27 @@ function CalcsTabClass:BuildOutput() self.miscCalculator = { self.calcs.getMiscCalculator(self.build) } end --- Controls the coroutine that calculates node power +-- Controls the coroutine (or worker job) that calculates node power function CalcsTabClass:BuildPower() + if self.powerBuildFlag and self.build.buildFlag then + -- A main output rebuild is pending and will set powerBuildFlag again + -- when it completes; starting now would immediately be cancelled and + -- restarted (which is expensive with background workers) + return + end if self.powerBuildFlag then self.powerBuildFlag = false self.powerMax = nil - self.powerBuilder = coroutine.create(self.PowerBuilder) + self.powerBuilder = nil + if self.powerJob then + -- A rebuild was requested while workers were still running; their + -- results are stale, so cancel and start over + self.powerJob:Cancel() + self.powerJob = nil + end + if not (ParallelRunner:IsAvailable() and self:LaunchParallelPowerBuild()) then + self.powerBuilder = coroutine.create(self.PowerBuilder) + end end if self.powerBuilder then local res, errMsg = coroutine.resume(self.powerBuilder, self) @@ -472,7 +488,10 @@ function CalcsTabClass:BuildPower() end -- Estimate the offensive and defensive power of all unallocated nodes -function CalcsTabClass:PowerBuilder() +-- filter is optional (used by parallel workers): { nodes = , +-- cluster = } restricts which candidates are +-- calculated; behaviour with no filter is unchanged +function CalcsTabClass:PowerBuilder(filter) -- local timer_start = GetTime() local useFullDPS = self.powerStat and self.powerStat.stat == "FullDPS" local calcFunc, calcBase = self:GetMiscCalculator() @@ -544,7 +563,7 @@ function CalcsTabClass:PowerBuilder() if node.type == "Mastery" then node.power.masteryEffects = { } end - if node.modKey ~= "" and not self.mainEnv.grantedPassives[nodeId] then + if node.modKey ~= "" and not self.mainEnv.grantedPassives[nodeId] and (not filter or filter.nodes[nodeId]) then if node.type == "Mastery" and node.allMasteryOptions then if not (self.nodePowerMaxDepth and self.nodePowerMaxDepth < node.pathDist) then t_insert(masteryNodeList, node) @@ -569,8 +588,8 @@ function CalcsTabClass:PowerBuilder() distanceMap = nil table.sort(distanceList, function(a, b) return a[1] < b[1] end) -- Count eligible cluster nodes - for _, node in pairs(self.build.spec.tree.clusterNodeMap) do - if not node.alloc and node.modKey ~= "" and not self.mainEnv.grantedPassives[node.id] then + for nodeName, node in pairs(self.build.spec.tree.clusterNodeMap) do + if not node.alloc and node.modKey ~= "" and not self.mainEnv.grantedPassives[node.id] and (not filter or filter.cluster[nodeName]) then total = total + 1 end end @@ -624,6 +643,9 @@ function CalcsTabClass:PowerBuilder() end end nodeIndex = nodeIndex + 1 + if self.workerProgressFunc and nodeIndex % 10 == 0 then + self.workerProgressFunc(nodeIndex, total) + end if coroutine.running() and GetTime() - start > 100 then if self.build.powerBuilderProgressCallback then self.build.powerBuilderProgressCallback(m_floor(nodeIndex/total*100)) @@ -669,6 +691,9 @@ function CalcsTabClass:PowerBuilder() end end nodeIndex = nodeIndex + 1 + if self.workerProgressFunc and nodeIndex % 10 == 0 then + self.workerProgressFunc(nodeIndex, total) + end if coroutine.running() and GetTime() - start > 100 then if self.build.powerBuilderProgressCallback then self.build.powerBuilderProgressCallback(m_floor(nodeIndex/total*100)) @@ -697,6 +722,9 @@ function CalcsTabClass:PowerBuilder() node.power.singleStat = self:CalculatePowerStat(self.powerStat, output, calcBase) end nodeIndex = nodeIndex + 1 + if self.workerProgressFunc and nodeIndex % 10 == 0 then + self.workerProgressFunc(nodeIndex, total) + end if coroutine.running() and GetTime() - start > 100 then if self.build.powerBuilderProgressCallback then self.build.powerBuilderProgressCallback(m_floor(nodeIndex/total*100)) @@ -709,6 +737,269 @@ function CalcsTabClass:PowerBuilder() self.powerMax = newPowerMax self.powerBuilderInitialized = true -- ConPrintf("Power Build time: %d ms", GetTime() - timer_start) + return newPowerMax +end + +-- Enumerate the candidates PowerBuilder would calculate, grouped so that nodes +-- sharing a modKey stay together (they share one cache entry in PowerBuilder). +-- Returns { groups = { { weight, nodeIds } ... }, clusterNames = { ... }, total } +function CalcsTabClass:EnumeratePowerCandidates() + local groupByKey = { } + local groups = { } + local clusterNames = { } + local total = 0 + local function nodeGroup(key) + if not groupByKey[key] then + groupByKey[key] = { weight = 0, nodeIds = { } } + t_insert(groups, groupByKey[key]) + end + return groupByKey[key] + end + for nodeId, node in pairs(self.build.spec.nodes) do + if node.modKey ~= "" and not self.mainEnv.grantedPassives[nodeId] + and not (self.nodePowerMaxDepth and self.nodePowerMaxDepth < (node.pathDist or 1000)) then + if node.type == "Mastery" and node.allMasteryOptions then + -- Mastery effect pseudo-nodes each cost one calculation + local count = 0 + for _, masteryEffect in ipairs(node.masteryEffects or { }) do + local assignedNodeId = isValueInTable(self.build.spec.masterySelections, masteryEffect.effect) + if not assignedNodeId or assignedNodeId == node.id then + count = count + 1 + end + end + if count > 0 then + local group = nodeGroup("mastery:"..nodeId) + group.weight = group.weight + count + t_insert(group.nodeIds, nodeId) + total = total + count + end + else + local group = nodeGroup(node.alloc and (node.modKey.."_remove") or node.modKey) + group.weight = group.weight + 1 + t_insert(group.nodeIds, nodeId) + total = total + 1 + end + end + end + for nodeName, node in pairs(self.build.spec.tree.clusterNodeMap) do + if not node.alloc and node.modKey ~= "" and not self.mainEnv.grantedPassives[node.id] then + t_insert(clusterNames, nodeName) + total = total + 1 + end + end + return { groups = groups, clusterNames = clusterNames, total = total } +end + +-- Distribute enumerated candidates across up to workerCount batches (heaviest +-- modKey groups first, so batches stay balanced), attaching each candidate's +-- live path data +function CalcsTabClass:BuildPowerBatches(cand, workerCount) + local batches = { } + for i = 1, workerCount do + batches[i] = { weight = 0, nodeIds = { }, clusterNames = { } } + end + table.sort(cand.groups, function(a, b) return a.weight > b.weight end) + for _, group in ipairs(cand.groups) do + local lightest = batches[1] + for i = 2, workerCount do + if batches[i].weight < lightest.weight then + lightest = batches[i] + end + end + lightest.weight = lightest.weight + group.weight + for _, nodeId in ipairs(group.nodeIds) do + t_insert(lightest.nodeIds, nodeId) + end + end + for i, name in ipairs(cand.clusterNames) do + t_insert(batches[((i - 1) % workerCount) + 1].clusterNames, name) + end + -- Drop empty batches, strip local weights + for i = #batches, 1, -1 do + if #batches[i].nodeIds == 0 and #batches[i].clusterNames == 0 then + t_remove(batches, i) + else + batches[i].weight = nil + end + end + -- Send each candidate's live path/depends node ids along with the batch. + -- A spec rebuilt from XML resolves equal-length shortest-path ties in a + -- different pairs() order, so without this workers could compute pathPower + -- along a different (equally short) path than the one the UI displays. + for _, batch in ipairs(batches) do + batch.paths = { } + batch.depends = { } + for _, nodeId in ipairs(batch.nodeIds) do + local node = self.build.spec.nodes[nodeId] + if node then + if node.alloc and node.depends then + local ids = { } + for _, depNode in ipairs(node.depends) do + t_insert(ids, depNode.id) + end + batch.depends[tostring(nodeId)] = ids + elseif node.path then + local ids = { } + for _, pathNode in ipairs(node.path) do + t_insert(ids, pathNode.id) + end + batch.paths[tostring(nodeId)] = ids + end + end + end + end + return batches +end + +-- Try to run the power build on background workers; returns true if a job was +-- launched, false if the caller should use the single-core coroutine instead +function CalcsTabClass:LaunchParallelPowerBuild() + local cand = self:EnumeratePowerCandidates() + if not ParallelRunner:ShouldParallelize(cand.total) then + return false + end + local buildXML = self.build:SaveDB("parallelPower") + if not buildXML then + return false + end + local revision = self.build.outputRevision + local calcFunc, calcBase = self:GetMiscCalculator() + local powerStat = self.powerStat + local useFullDPS = powerStat and powerStat.stat == "FullDPS" + local powerStatIndex + if powerStat then + for i, stat in ipairs(data.powerStatList) do + if stat == powerStat then + powerStatIndex = i + break + end + end + end + local batches = self:BuildPowerBatches(cand, ParallelRunner:GetWorkerCount()) + -- The tree view reads powerMax while the heat map is enabled; the coroutine + -- path initializes it on its first resume, so the worker path must provide + -- the same neutral values until the merge overwrites them + local neutralPowerMax = { + singleStat = 0, + offence = 0, + offencePerPoint = 0, + defence = 0, + defencePerPoint = 0 + } + local job = ParallelRunner:LaunchJob({ + mode = "nodePower", + buildXML = buildXML, + common = { + powerStatIndex = powerStatIndex, + nodePowerMaxDepth = self.nodePowerMaxDepth, + }, + batches = batches, + total = cand.total, + expectedBaseline = PowerCalcTasks.extractBaseline(calcBase, powerStat, useFullDPS), + onProgress = function(percent) + if self.build.powerBuilderProgressCallback then + self.build.powerBuilderProgressCallback(percent) + end + end, + onComplete = function(payloads) + self.powerJob = nil + self:MergeParallelPower(payloads, revision) + end, + onError = function(errMsg) + -- Fall back to the single-core coroutine path + self.powerJob = nil + self.powerBuilder = coroutine.create(self.PowerBuilder) + end, + }) + if not job then + return false + end + self.powerMax = neutralPowerMax + self.powerJob = job + return true +end + +-- Apply worker results onto the live build; mirrors the state PowerBuilder +-- leaves behind (node.power tables, powerMax, completion callback) +function CalcsTabClass:MergeParallelPower(payloads, revision) + if self.build.outputRevision ~= revision then + -- The build changed while workers were running; discard and rebuild + ParallelRunner:Log("merge discarded: revision %s vs %s", tostring(revision), tostring(self.build.outputRevision)) + self.powerBuildFlag = true + return + end + local spec = self.build.spec + local decodeNumber = PowerCalcTasks.decodeNumber + for nodeId, node in pairs(spec.nodes) do + wipeTable(node.power) + if node.type == "Mastery" then + node.power.masteryEffects = { } + end + end + for _, node in pairs(spec.tree.clusterNodeMap) do + if node.power then + wipeTable(node.power) + else + node.power = { } + end + end + local newPowerMax = { + singleStat = 0, + offence = 0, + offencePerPoint = 0, + defence = 0, + defencePerPoint = 0 + } + for _, payload in pairs(payloads) do + local results = payload.results or { } + for idStr, power in pairs(results.nodes or { }) do + local node = spec.nodes[tonumber(idStr)] + if node then + node.power.singleStat = decodeNumber(power.singleStat) + node.power.pathPower = decodeNumber(power.pathPower) + node.power.offence = decodeNumber(power.offence) + node.power.defence = decodeNumber(power.defence) + if power.masteryEffects then + node.power.masteryEffects = node.power.masteryEffects or { } + for effIdStr, effect in pairs(power.masteryEffects) do + node.power.masteryEffects[tonumber(effIdStr)] = { + singleStat = decodeNumber(effect.singleStat), + pathPower = decodeNumber(effect.pathPower), + offence = decodeNumber(effect.offence), + defence = decodeNumber(effect.defence), + } + end + end + end + end + for name, power in pairs(results.cluster or { }) do + local node = spec.tree.clusterNodeMap[name] + if node then + node.power = node.power or { } + node.power.singleStat = decodeNumber(power.singleStat) + end + end + if results.powerMax then + for key in pairs(newPowerMax) do + newPowerMax[key] = m_max(newPowerMax[key], decodeNumber(results.powerMax[key]) or 0) + end + end + end + local mergedNodes, poweredNodes = 0, 0 + for _, payload in pairs(payloads) do + for _, power in pairs((payload.results or { }).nodes or { }) do + mergedNodes = mergedNodes + 1 + if power.singleStat then + poweredNodes = poweredNodes + 1 + end + end + end + ParallelRunner:Log("merge applied: %d node results (%d with singleStat), powerMax.singleStat=%s", mergedNodes, poweredNodes, tostring(newPowerMax.singleStat)) + self.powerMax = newPowerMax + self.powerBuilderInitialized = true + if self.build.powerBuilderCallback then + self.build.powerBuilderCallback() + end end function CalcsTabClass:CalculatePowerStat(selection, original, modified) diff --git a/src/Classes/CompareTab.lua b/src/Classes/CompareTab.lua index 24506313c6c..12466f08434 100644 --- a/src/Classes/CompareTab.lua +++ b/src/Classes/CompareTab.lua @@ -2435,39 +2435,21 @@ function CompareTabClass:GetSocketGroupLabel(group) return table.concat(names, " + ") end --- Coroutine: calculate power of compared build elements against primary build -function CompareTabClass:ComparePowerBuilder(compareEntry, powerStat, categories) - local results = {} - local useFullDPS = powerStat.stat == "FullDPS" - - -- Get calculator for primary build - local calcFunc, calcBase = self.calcs.getMiscCalculator(self.primaryBuild) - - -- Find display stat for formatting - local displayStat = nil - for _, ds in ipairs(self.primaryBuild.displayStats) do - if ds.stat == powerStat.stat then - displayStat = ds - break - end - end - if not displayStat then - displayStat = { fmt = ".1f" } - end - - local total = 0 - local processed = 0 - local start = GetTime() - - -- Count total work items for progress +-- Enumerate the candidates the compare power report would calculate, as an +-- ordered list of descriptors. Descriptors carry only stable identifiers +-- (node ids, slot names, list indices, config var names) so they can be sent +-- to worker VMs which resolve them against their own reconstructed builds. +function CompareTabClass:EnumerateComparePowerCandidates(compareEntry, categories) + local descriptors = {} if categories.treeNodes then local compareNodes = compareEntry.spec and compareEntry.spec.allocNodes or {} local primaryNodes = self.primaryBuild.spec and self.primaryBuild.spec.allocNodes or {} - for nodeId, node in pairs(compareNodes) do + for nodeId, _ in pairs(compareNodes) do if type(nodeId) == "number" and nodeId < CLUSTER_NODE_OFFSET and not primaryNodes[nodeId] then local pNode = self.primaryBuild.spec.nodes[nodeId] - if pNode and (pNode.type == "Normal" or pNode.type == "Notable" or pNode.type == "Keystone") and not pNode.ascendancyName then - total = total + 1 + if pNode and (pNode.type == "Normal" or pNode.type == "Notable" or pNode.type == "Keystone") + and not pNode.ascendancyName and pNode.modKey ~= "" then + t_insert(descriptors, { kind = "treeNode", nodeId = nodeId }) end end end @@ -2477,34 +2459,43 @@ function CompareTabClass:ComparePowerBuilder(compareEntry, powerStat, categories if self:ShouldShowRing3(compareEntry) then t_insert(baseSlots, 10, "Ring 3") end - -- we only use jewel slots if both sides have them available self:AddAbyssSockets(compareEntry, baseSlots, true) - for _, slotName in ipairs(baseSlots) do local cSlot = compareEntry.itemsTab and compareEntry.itemsTab.slots[slotName] local cItem = cSlot and compareEntry.itemsTab.items[cSlot.selItemId] - if cItem then - total = total + 1 + local pSlot = self.primaryBuild.itemsTab and self.primaryBuild.itemsTab.slots[slotName] + local pItem = pSlot and self.primaryBuild.itemsTab.items[pSlot.selItemId] + if cItem and cItem.raw and not (pItem and pItem.name == cItem.name) then + t_insert(descriptors, { kind = "item", slotName = slotName }) end end - -- Count jewels for progress tracking - local jewelSlots = self:GetJewelComparisonSlots(compareEntry) - for _, jEntry in ipairs(jewelSlots) do - if jEntry.cItem then - total = total + 1 + -- Jewels; identified by their socket node id (GetJewelComparisonSlots + -- returns a nodeId-sorted list in any VM) + for _, jEntry in ipairs(self:GetJewelComparisonSlots(compareEntry)) do + if jEntry.cItem and jEntry.cItem.raw and not (jEntry.pItem and jEntry.pItem.name == jEntry.cItem.name) then + t_insert(descriptors, { kind = "jewel", nodeId = jEntry.nodeId }) end end end if categories.skillGems then local cGroups = compareEntry.skillsTab and compareEntry.skillsTab.socketGroupList or {} - total = total + #cGroups + local pGroups = self.primaryBuild.skillsTab and self.primaryBuild.skillsTab.socketGroupList or {} + local pSignatures = {} + for _, group in ipairs(pGroups) do + pSignatures[self:GetSocketGroupSignature(group)] = true + end + for groupIndex, cGroup in ipairs(cGroups) do + local sig = self:GetSocketGroupSignature(cGroup) + if sig ~= "" and not pSignatures[sig] then + t_insert(descriptors, { kind = "skillGem", groupIndex = groupIndex }) + end + end end if categories.supportGems then local cMainGroup = compareEntry.skillsTab and compareEntry.skillsTab.socketGroupList[compareEntry.mainSocketGroup] local pMainGroup = self.primaryBuild.skillsTab and self.primaryBuild.skillsTab.socketGroupList[self.primaryBuild.mainSocketGroup] if cMainGroup and pMainGroup then - -- Count support gems in compared build's main group not in primary's main group local pSupportNames = {} for _, gem in ipairs(pMainGroup.gemList or {}) do local ge = self:GetGemGrantedEffect(gem) @@ -2513,12 +2504,12 @@ function CompareTabClass:ComparePowerBuilder(compareEntry, powerStat, categories if name then pSupportNames[name] = true end end end - for _, gem in ipairs(cMainGroup.gemList or {}) do - local ge = self:GetGemGrantedEffect(gem) + for gemIndex, cGem in ipairs(cMainGroup.gemList or {}) do + local ge = self:GetGemGrantedEffect(cGem) if ge and ge.support then - local name = ge.name or gem.nameSpec + local name = ge.name or cGem.nameSpec if name and not pSupportNames[name] then - total = total + 1 + t_insert(descriptors, { kind = "supportGem", gemIndex = gemIndex }) end end end @@ -2531,524 +2522,609 @@ function CompareTabClass:ComparePowerBuilder(compareEntry, powerStat, categories if varData.var and varData.apply and varData.type ~= "text" then local pVal, cVal = self:NormalizeConfigVals(varData, pInput[varData.var], cInput[varData.var]) if pVal ~= cVal then - total = total + 1 + t_insert(descriptors, { kind = "config", var = varData.var }) end end end end + return descriptors +end - if total == 0 then - self.comparePowerResults = results - self.comparePowerProgress = 100 - return - end +-- Create the shared calculation context for computing compare power candidates +function CompareTabClass:MakeComparePowerContext(compareEntry, powerStat) + local ctx = { + compareEntry = compareEntry, + powerStat = powerStat, + useFullDPS = powerStat.stat == "FullDPS", + cache = {}, + } + ctx.calcFunc, ctx.calcBase = self.calcs.getMiscCalculator(self.primaryBuild) + return ctx +end - -- Get baseline stat value for percentage calculation +-- Create the formatting context used to turn raw impact numbers into result +-- rows; main thread only (formatting depends on UI settings) +function CompareTabClass:MakeComparePowerFormatContext(compareEntry, powerStat, calcBase) + local displayStat = nil + for _, ds in ipairs(self.primaryBuild.displayStats) do + if ds.stat == powerStat.stat then + displayStat = ds + break + end + end + if not displayStat then + displayStat = { fmt = ".1f" } + end local baseStatValue = calcBase[powerStat.stat] or 0 if powerStat.transform then baseStatValue = powerStat.transform(baseStatValue) end + return { + compareEntry = compareEntry, + powerStat = powerStat, + displayStat = displayStat, + baseStatValue = baseStatValue, + } +end - -- Helper to format an impact value and compute percentage - local function formatImpact(impact) - local displayVal = impact * ((displayStat.pc or displayStat.mod) and 100 or 1) - local rawNumStr = s_format("%" .. displayStat.fmt, displayVal) - local isZero = (tonumber(rawNumStr) == 0) - local numStr = formatNumSep(rawNumStr) - - -- Determine color - local isPositive = (displayVal > 0 and not displayStat.lowerIsBetter) or (displayVal < 0 and displayStat.lowerIsBetter) - local isNegative = (displayVal < 0 and not displayStat.lowerIsBetter) or (displayVal > 0 and displayStat.lowerIsBetter) - local color = isPositive and colorCodes.POSITIVE or isNegative and colorCodes.NEGATIVE or "^7" - local sign = displayVal > 0 and "+" or "" - local str = color .. sign .. numStr - - -- Compute percentage change - local percent = 0 - if baseStatValue ~= 0 then - percent = (impact / math.abs(baseStatValue)) * 100 - end +-- Format an impact value and compute percentage +function CompareTabClass:FormatComparePowerImpact(fmtCtx, impact) + local displayStat = fmtCtx.displayStat + local displayVal = impact * ((displayStat.pc or displayStat.mod) and 100 or 1) + local rawNumStr = s_format("%" .. displayStat.fmt, displayVal) + local isZero = (tonumber(rawNumStr) == 0) + local numStr = formatNumSep(rawNumStr) - -- Build combined string: "+1,234.5 (+4.3%)" - local combinedStr = str - if percent ~= 0 then - local pctStr = s_format("%+.1f%%", percent) - combinedStr = str .. " ^7(" .. color .. pctStr .. "^7)" - end + -- Determine color + local isPositive = (displayVal > 0 and not displayStat.lowerIsBetter) or (displayVal < 0 and displayStat.lowerIsBetter) + local isNegative = (displayVal < 0 and not displayStat.lowerIsBetter) or (displayVal > 0 and displayStat.lowerIsBetter) + local color = isPositive and colorCodes.POSITIVE or isNegative and colorCodes.NEGATIVE or "^7" + local sign = displayVal > 0 and "+" or "" + local str = color .. sign .. numStr - return str, displayVal, combinedStr, percent, isZero + -- Compute percentage change + local percent = 0 + if fmtCtx.baseStatValue ~= 0 then + percent = (impact / math.abs(fmtCtx.baseStatValue)) * 100 end - -- ========================================== - -- Tree Nodes - -- ========================================== - if categories.treeNodes then - local compareNodes = compareEntry.spec and compareEntry.spec.allocNodes or {} - local primaryNodes = self.primaryBuild.spec and self.primaryBuild.spec.allocNodes or {} - local cache = {} + -- Build combined string: "+1,234.5 (+4.3%)" + local combinedStr = str + if percent ~= 0 then + local pctStr = s_format("%+.1f%%", percent) + combinedStr = str .. " ^7(" .. color .. pctStr .. "^7)" + end - for nodeId, _ in pairs(compareNodes) do - if type(nodeId) == "number" and nodeId < CLUSTER_NODE_OFFSET and not primaryNodes[nodeId] then - local pNode = self.primaryBuild.spec.nodes[nodeId] - if pNode and (pNode.type == "Normal" or pNode.type == "Notable" or pNode.type == "Keystone") - and not pNode.ascendancyName and pNode.modKey ~= "" then - local output - if cache[pNode.modKey] then - output = cache[pNode.modKey] - else - output = calcFunc({ addNodes = { [pNode] = true } }, useFullDPS) - cache[pNode.modKey] = output - end - local impact = self.primaryBuild.calcsTab:CalculatePowerStat(powerStat, output, calcBase) - local pathDist = pNode.pathDist or 0 - if pathDist == 0 then - pathDist = #(pNode.path or {}) - if pathDist == 0 then pathDist = 1 end - end - local perPoint = impact / pathDist - local impactStr, impactVal, combinedImpactStr, impactPercent, impactIsZero = formatImpact(impact) - local perPointStr = formatImpact(perPoint) - - if not impactIsZero then - t_insert(results, { - category = "Tree", - categoryColor = "^7", - nameColor = "^7", - name = pNode.dn, - nodeId = nodeId, - impact = impactVal, - impactStr = impactStr, - impactPercent = impactPercent, - combinedImpactStr = combinedImpactStr, - pathDist = pathDist, - perPoint = perPoint * ((displayStat.pc or displayStat.mod) and 100 or 1), - perPointStr = perPointStr, - }) - end + return str, displayVal, combinedStr, percent, isZero +end - processed = processed + 1 - if coroutine.running() and GetTime() - start > 100 then - self.comparePowerProgress = m_floor(processed / total * 100) - coroutine.yield() - start = GetTime() +-- Compute the raw impact of one candidate descriptor against the primary +-- build. Runs identically on the main thread and inside worker VMs. +-- Returns ok, impact, extra: ok=false means the calculation errored (impact +-- holds the message); impact=nil means the candidate resolved to nothing. +function CompareTabClass:ComputeComparePowerCandidate(ctx, desc) + local compareEntry = ctx.compareEntry + if desc.kind == "treeNode" then + local pNode = self.primaryBuild.spec.nodes[desc.nodeId] + if not (pNode and pNode.modKey ~= "") then + return true, nil + end + local output = ctx.cache[pNode.modKey] + if not output then + output = ctx.calcFunc({ addNodes = { [pNode] = true } }, ctx.useFullDPS) + ctx.cache[pNode.modKey] = output + end + local impact = self.primaryBuild.calcsTab:CalculatePowerStat(ctx.powerStat, output, ctx.calcBase) + local pathDist = pNode.pathDist or 0 + if pathDist == 0 then + pathDist = #(pNode.path or {}) + if pathDist == 0 then pathDist = 1 end + end + return true, impact, { pathDist = pathDist } + elseif desc.kind == "item" then + local slotName = desc.slotName + local cSlot = compareEntry.itemsTab and compareEntry.itemsTab.slots[slotName] + local cItem = cSlot and compareEntry.itemsTab.items[cSlot.selItemId] + if not (cItem and cItem.raw) then + return true, nil + end + local newItem = new("Item", cItem.raw) + newItem:NormaliseQuality() + + -- if our comparison has abyssal jewels, but the primary build + -- doesn't, add those temporarily to the build to work around + -- calcfunc not being able to take in multiple items + local cmpJewels = {} + local oldEquipped = {} + if newItem.abyssalSocketCount > 0 then + for idx = 1, newItem.abyssalSocketCount do + local abyssSlotName = string.format("%s Abyssal Socket %d", slotName, idx) + local cmpJewelSlot = compareEntry.itemsTab.slots[abyssSlotName] + -- save old id and unequip existing + local primaryJewelSlot = self.primaryBuild.itemsTab.slots[abyssSlotName] + oldEquipped[abyssSlotName] = primaryJewelSlot.selItemId + primaryJewelSlot:SetSelItemId(0) + if cmpJewelSlot.selItemId > 0 then + local cmpJewel = compareEntry.itemsTab.items[cmpJewelSlot.selItemId] + -- due to a previous bug where jewel slots didn't + -- get cleared when becoming inactive, so the item + -- might not exist + if cmpJewel then + -- copy item + local itemCopy = new("Item", cmpJewel:BuildRaw()) + table.insert(cmpJewels, itemCopy) + self.primaryBuild.itemsTab:AddItem(itemCopy, false) + + -- equip copied + self.primaryBuild.itemsTab.slots[abyssSlotName]:SetSelItemId(itemCopy.id) end end end end - end - -- ========================================== - -- Items - -- ========================================== - if categories.items then - local baseSlots = { "Weapon 1", "Weapon 2", "Helmet", "Body Armour", "Gloves", "Boots", "Amulet", "Ring 1", "Ring 2", "Belt", "Flask 1", "Flask 2", "Flask 3", "Flask 4", "Flask 5" } - if self:ShouldShowRing3(compareEntry) then - t_insert(baseSlots, 10, "Ring 3") - end + local output = ctx.calcFunc({ repSlotName = slotName, repItem = newItem }, ctx.useFullDPS) + local impact = self.primaryBuild.calcsTab:CalculatePowerStat(ctx.powerStat, output, ctx.calcBase) - -- we only use jewel slots if both sides have them available - self:AddAbyssSockets(compareEntry, baseSlots, true) - - for _, slotName in ipairs(baseSlots) do - local cSlot = compareEntry.itemsTab and compareEntry.itemsTab.slots[slotName] - local cItem = cSlot and compareEntry.itemsTab.items[cSlot.selItemId] - local pSlot = self.primaryBuild.itemsTab and self.primaryBuild.itemsTab.slots[slotName] - local pItem = pSlot and self.primaryBuild.itemsTab.items[pSlot.selItemId] - if cItem and cItem.raw and not (pItem and pItem.name == cItem.name) then - local newItem = new("Item", cItem.raw) - newItem:NormaliseQuality() - - -- if our comparison has abyssal jewels, but the primary build - -- doesn't, add those temporarily to the build to work around - -- calcfunc not being able to take in multiple items - local cmpJewels = {} - local oldEquipped = {} - if newItem.abyssalSocketCount > 0 then - for idx = 1, newItem.abyssalSocketCount do - local abyssSlotName = string.format("%s Abyssal Socket %d", slotName, idx) - local cmpJewelSlot = compareEntry.itemsTab.slots[abyssSlotName] - -- save old id and unequip existing - local primaryJewelSlot = self.primaryBuild.itemsTab.slots[abyssSlotName] - oldEquipped[abyssSlotName] = primaryJewelSlot.selItemId - primaryJewelSlot:SetSelItemId(0) - if cmpJewelSlot.selItemId > 0 then - local cmpJewel = compareEntry.itemsTab.items[cmpJewelSlot.selItemId] - -- due to a previous bug where jewel slots didn't - -- get cleared when becoming inactive, so the item - -- might not exist - if cmpJewel then - -- copy item - local itemCopy = new("Item", cmpJewel:BuildRaw()) - table.insert(cmpJewels, itemCopy) - self.primaryBuild.itemsTab:AddItem(itemCopy, false) - - -- equip copied - self.primaryBuild.itemsTab.slots[abyssSlotName]:SetSelItemId(itemCopy.id) - end + -- restore abyss jewel state + if newItem.abyssalSocketCount > 0 then + for k, v in pairs(oldEquipped) do + self.primaryBuild.itemsTab.slots[k]:SetSelItemId(v) + end + for _, item in ipairs(cmpJewels) do + self.primaryBuild.itemsTab:DeleteItem(item) + end + end + return true, impact, { cmpJewelCount = #cmpJewels } + elseif desc.kind == "jewel" then + local nodeId = desc.nodeId + local cSpec = compareEntry.spec + local cItemId = cSpec and cSpec.jewels and cSpec.jewels[nodeId] + local cItem = cItemId and compareEntry.itemsTab.items[cItemId] + if not (cItem and cItem.raw) then + return true, nil + end + local newItem = new("Item", cItem.raw) + newItem:NormaliseQuality() + local pSpec = self.primaryBuild.spec + local pNodeAllocated = pSpec.allocNodes and pSpec.allocNodes[nodeId] and true or false + local bestImpactVal = nil + if pNodeAllocated then + -- Socket is allocated in primary build, test directly in that socket + local output = ctx.calcFunc({ repSlotName = "Jewel "..nodeId, repItem = newItem }, ctx.useFullDPS) + bestImpactVal = self.primaryBuild.calcsTab:CalculatePowerStat(ctx.powerStat, output, ctx.calcBase) + else + -- Socket is NOT allocated in primary build; try the jewel in every + -- jewel socket on the primary build's tree, temporarily allocating + -- unallocated sockets via addNodes so CalcSetup doesn't skip them + if not ctx.primaryJewelSockets then + ctx.primaryJewelSockets = {} + for _, slot in ipairs(self.primaryBuild.itemsTab.orderedSlots) do + if slot.nodeId then + local node = pSpec.nodes[slot.nodeId] + if node then + t_insert(ctx.primaryJewelSockets, { + slotName = slot.slotName, + nodeId = slot.nodeId, + node = node, + allocated = pSpec.allocNodes and pSpec.allocNodes[slot.nodeId] and true or false, + }) end end end - - - local output = calcFunc({ repSlotName = slotName, repItem = newItem }, useFullDPS) - local impact = self.primaryBuild.calcsTab:CalculatePowerStat(powerStat, output, calcBase) - local impactStr, impactVal, combinedImpactStr, impactPercent, impactIsZero = formatImpact(impact) - - -- restore abyss jewel state - if newItem.abyssalSocketCount > 0 then - for k, v in pairs(oldEquipped) do - self.primaryBuild.itemsTab.slots[k]:SetSelItemId(v) - end - for _, item in ipairs(cmpJewels) do - self.primaryBuild.itemsTab:DeleteItem(item) - end + end + for _, socketInfo in ipairs(ctx.primaryJewelSockets) do + local override = { repSlotName = socketInfo.slotName, repItem = newItem } + if not socketInfo.allocated then + override.addNodes = { [socketInfo.node] = true } end - - if not impactIsZero then - -- Get rarity color for item name - local rarityColor = colorCodes[cItem.rarity] or colorCodes.NORMAL - - - local abyssJewelText = #cmpJewels > 0 and - s_format(", %d Jewel%s", #cmpJewels, #cmpJewels > 1 and "s" or "") or "" - t_insert(results, { - category = "Item", - categoryColor = rarityColor, - nameColor = rarityColor, - name = (cItem.name or "Unknown") .. ", " .. slotName .. abyssJewelText, - itemObj = newItem, - slotName = slotName, - impact = impactVal, - impactStr = impactStr, - impactPercent = impactPercent, - combinedImpactStr = combinedImpactStr, - pathDist = nil, - perPoint = nil, - perPointStr = nil, - }) + local output = ctx.calcFunc(override, ctx.useFullDPS) + local impact = self.primaryBuild.calcsTab:CalculatePowerStat(ctx.powerStat, output, ctx.calcBase) + if bestImpactVal == nil or impact > bestImpactVal then + bestImpactVal = impact end end - processed = processed + 1 - if coroutine.running() and GetTime() - start > 100 then - self.comparePowerProgress = m_floor(processed / total * 100) - coroutine.yield() - start = GetTime() - end end - end + return true, bestImpactVal, { bestSocket = not pNodeAllocated } + elseif desc.kind == "skillGem" then + local cGroup = compareEntry.skillsTab and compareEntry.skillsTab.socketGroupList[desc.groupIndex] + if not cGroup then + return true, nil + end + local pGroups = self.primaryBuild.skillsTab and self.primaryBuild.skillsTab.socketGroupList or {} + -- Temporarily add this socket group to primary build and recalculate + t_insert(pGroups, cGroup) + self.primaryBuild.buildFlag = true - -- ========================================== - -- Jewels (included as items) - -- ========================================== - if categories.items then - -- Build list of jewel socket info in the primary build for fallback testing - -- Each entry has { slotName, nodeId, node, allocated } - local pSpec = self.primaryBuild.spec - local primaryJewelSockets = {} - for _, slot in ipairs(self.primaryBuild.itemsTab.orderedSlots) do - if slot.nodeId then - local node = pSpec.nodes[slot.nodeId] - local allocated = pSpec.allocNodes and pSpec.allocNodes[slot.nodeId] and true or false - if node then - t_insert(primaryJewelSockets, { - slotName = slot.slotName, - nodeId = slot.nodeId, - node = node, - allocated = allocated, - }) - end - end + -- Get a fresh calculator with the added group (pcall to guarantee cleanup) + local ok, gemCalcFunc, gemCalcBase = pcall(function() + return self.calcs.getMiscCalculator(self.primaryBuild) + end) + + -- Always remove the temporarily added group + t_remove(pGroups) + self.primaryBuild.buildFlag = true + + if not ok then + -- gemCalcFunc contains the error message on failure + return false, tostring(gemCalcFunc) end + return true, self.primaryBuild.calcsTab:CalculatePowerStat(ctx.powerStat, gemCalcBase, ctx.calcBase) + elseif desc.kind == "supportGem" then + local cMainGroup = compareEntry.skillsTab and compareEntry.skillsTab.socketGroupList[compareEntry.mainSocketGroup] + local pMainGroup = self.primaryBuild.skillsTab and self.primaryBuild.skillsTab.socketGroupList[self.primaryBuild.mainSocketGroup] + local cGem = cMainGroup and cMainGroup.gemList and cMainGroup.gemList[desc.gemIndex] + if not (cGem and pMainGroup) then + return true, nil + end + -- Create a temporary copy of this support gem + local tempGem = { + nameSpec = cGem.nameSpec, + level = cGem.level, + quality = cGem.quality, + qualityId = cGem.qualityId, + enabled = cGem.enabled, + grantedEffect = cGem.grantedEffect, + gemData = cGem.gemData, + count = cGem.count, + enableGlobal1 = cGem.enableGlobal1, + enableGlobal2 = cGem.enableGlobal2, + } - local jewelSlots = self:GetJewelComparisonSlots(compareEntry) - for _, jEntry in ipairs(jewelSlots) do - if jEntry.cItem and jEntry.cItem.raw and not (jEntry.pItem and jEntry.pItem.name == jEntry.cItem.name) then - local newItem = new("Item", jEntry.cItem.raw) - newItem:NormaliseQuality() + -- Temporarily add to primary build's main socket group + t_insert(pMainGroup.gemList, tempGem) + self.primaryBuild.buildFlag = true - local bestImpactVal = nil - local bestSlotLabel = jEntry.label + local ok, sgCalcFunc, sgCalcBase = pcall(function() + return self.calcs.getMiscCalculator(self.primaryBuild) + end) - if jEntry.pNodeAllocated then - -- Socket is allocated in primary build, test directly in that socket - local output = calcFunc({ repSlotName = jEntry.cSlotName, repItem = newItem }, useFullDPS) - bestImpactVal = self.primaryBuild.calcsTab:CalculatePowerStat(powerStat, output, calcBase) - else - -- Socket is NOT allocated in primary build; try the jewel in every - -- jewel socket on the primary build's tree, temporarily allocating - -- unallocated sockets via addNodes so CalcSetup doesn't skip them - for _, socketInfo in ipairs(primaryJewelSockets) do - local override = { repSlotName = socketInfo.slotName, repItem = newItem } - if not socketInfo.allocated then - override.addNodes = { [socketInfo.node] = true } - end - local output = calcFunc(override, useFullDPS) - local impact = self.primaryBuild.calcsTab:CalculatePowerStat(powerStat, output, calcBase) - if bestImpactVal == nil or impact > bestImpactVal then - bestImpactVal = impact - bestSlotLabel = jEntry.label .. " (best socket)" - end - end - end + -- Always remove the temporarily added gem + t_remove(pMainGroup.gemList) + self.primaryBuild.buildFlag = true - if bestImpactVal ~= nil then - local impactStr, impactVal, combinedImpactStr, impactPercent, impactIsZero = formatImpact(bestImpactVal) - if not impactIsZero then - local rarityColor = colorCodes[jEntry.cItem.rarity] or colorCodes.NORMAL - - t_insert(results, { - category = "Item", - categoryColor = rarityColor, - nameColor = rarityColor, - name = (jEntry.cItem.name or "Unknown") .. ", " .. bestSlotLabel, - itemObj = newItem, - impact = impactVal, - impactStr = impactStr, - impactPercent = impactPercent, - combinedImpactStr = combinedImpactStr, - pathDist = nil, - perPoint = nil, - perPointStr = nil, - }) - end - end - end - processed = processed + 1 - if coroutine.running() and GetTime() - start > 100 then - self.comparePowerProgress = m_floor(processed / total * 100) - coroutine.yield() - start = GetTime() + if not ok then + return false, tostring(sgCalcFunc) + end + return true, self.primaryBuild.calcsTab:CalculatePowerStat(ctx.powerStat, sgCalcBase, ctx.calcBase) + elseif desc.kind == "config" then + local varData + for _, vd in ipairs(self.configOptions) do + if vd.var == desc.var then + varData = vd + break end end - end - - -- ========================================== - -- Skill Gems (socket groups) - -- ========================================== - if categories.skillGems then - local cGroups = compareEntry.skillsTab and compareEntry.skillsTab.socketGroupList or {} - local pGroups = self.primaryBuild.skillsTab and self.primaryBuild.skillsTab.socketGroupList or {} - - -- Build signature set for primary groups - local pSignatures = {} - for _, group in ipairs(pGroups) do - pSignatures[self:GetSocketGroupSignature(group)] = true + if not varData then + return true, nil end + local pInput = self.primaryBuild.configTab.input + local cInput = compareEntry.configTab.input or {} + local cVal = cInput[varData.var] - for _, cGroup in ipairs(cGroups) do - local sig = self:GetSocketGroupSignature(cGroup) - if sig ~= "" and not pSignatures[sig] then - -- Temporarily add this socket group to primary build and recalculate - t_insert(pGroups, cGroup) - self.primaryBuild.buildFlag = true + -- Save original value + local savedVal = pInput[varData.var] - -- Get a fresh calculator with the added group (pcall to guarantee cleanup) - local ok, gemCalcFunc, gemCalcBase = pcall(function() - return self.calcs.getMiscCalculator(self.primaryBuild) - end) + -- Apply compare build's config value + pInput[varData.var] = cVal - -- Always remove the temporarily added group - t_remove(pGroups) - self.primaryBuild.buildFlag = true + -- Rebuild and calculate (pcall to guarantee restore on error) + local ok, cfgCalcFunc, cfgCalcBase = pcall(function() + self.primaryBuild.configTab:BuildModList() + self.primaryBuild.buildFlag = true + return self.calcs.getMiscCalculator(self.primaryBuild) + end) - if not ok then - -- gemCalcFunc contains the error message on failure; skip this group - ConPrintf("Compare power (gem): %s", tostring(gemCalcFunc)) - else - local impact = self.primaryBuild.calcsTab:CalculatePowerStat(powerStat, gemCalcBase, calcBase) - local impactStr, impactVal, combinedImpactStr, impactPercent, impactIsZero = formatImpact(impact) - if not impactIsZero then - local label = self:GetSocketGroupLabel(cGroup) - - t_insert(results, { - category = "Skill gem", - categoryColor = colorCodes.GEM, - nameColor = colorCodes.GEM, - name = label, - impact = impactVal, - impactStr = impactStr, - impactPercent = impactPercent, - combinedImpactStr = combinedImpactStr, - pathDist = nil, - perPoint = nil, - perPointStr = nil, - }) - end - end - end - processed = processed + 1 - if coroutine.running() and GetTime() - start > 100 then - self.comparePowerProgress = m_floor(processed / total * 100) - coroutine.yield() - start = GetTime() - end + -- Always restore original value + pInput[varData.var] = savedVal + self.primaryBuild.configTab:BuildModList() + self.primaryBuild.buildFlag = true + + if not ok then + return false, tostring(cfgCalcFunc) end + return true, self.primaryBuild.calcsTab:CalculatePowerStat(ctx.powerStat, cfgCalcBase, ctx.calcBase) end + return true, nil +end - -- ========================================== - -- Support Gems (from compared build's active skill) - -- ========================================== - if categories.supportGems then +-- Turn a computed impact into a result row for the report list; returns nil +-- when the (formatted) impact is zero. Main thread only. +function CompareTabClass:BuildComparePowerRow(desc, impact, extra, fmtCtx) + local compareEntry = fmtCtx.compareEntry + local impactStr, impactVal, combinedImpactStr, impactPercent, impactIsZero = self:FormatComparePowerImpact(fmtCtx, impact) + if impactIsZero then + return nil + end + if desc.kind == "treeNode" then + local pNode = self.primaryBuild.spec.nodes[desc.nodeId] + if not pNode then + return nil + end + local pathDist = extra.pathDist or 1 + local perPoint = impact / pathDist + local perPointStr = self:FormatComparePowerImpact(fmtCtx, perPoint) + return { + category = "Tree", + categoryColor = "^7", + nameColor = "^7", + name = pNode.dn, + nodeId = desc.nodeId, + impact = impactVal, + impactStr = impactStr, + impactPercent = impactPercent, + combinedImpactStr = combinedImpactStr, + pathDist = pathDist, + perPoint = perPoint * ((fmtCtx.displayStat.pc or fmtCtx.displayStat.mod) and 100 or 1), + perPointStr = perPointStr, + } + elseif desc.kind == "item" then + local cSlot = compareEntry.itemsTab and compareEntry.itemsTab.slots[desc.slotName] + local cItem = cSlot and compareEntry.itemsTab.items[cSlot.selItemId] + if not (cItem and cItem.raw) then + return nil + end + local newItem = new("Item", cItem.raw) + newItem:NormaliseQuality() + local rarityColor = colorCodes[cItem.rarity] or colorCodes.NORMAL + local cmpJewelCount = extra.cmpJewelCount or 0 + local abyssJewelText = cmpJewelCount > 0 and + s_format(", %d Jewel%s", cmpJewelCount, cmpJewelCount > 1 and "s" or "") or "" + return { + category = "Item", + categoryColor = rarityColor, + nameColor = rarityColor, + name = (cItem.name or "Unknown") .. ", " .. desc.slotName .. abyssJewelText, + itemObj = newItem, + slotName = desc.slotName, + impact = impactVal, + impactStr = impactStr, + impactPercent = impactPercent, + combinedImpactStr = combinedImpactStr, + pathDist = nil, + perPoint = nil, + perPointStr = nil, + } + elseif desc.kind == "jewel" then + if not fmtCtx.jewelEntryByNode then + fmtCtx.jewelEntryByNode = {} + for _, jEntry in ipairs(self:GetJewelComparisonSlots(compareEntry)) do + fmtCtx.jewelEntryByNode[jEntry.nodeId] = jEntry + end + end + local jEntry = fmtCtx.jewelEntryByNode[desc.nodeId] + if not (jEntry and jEntry.cItem and jEntry.cItem.raw) then + return nil + end + local newItem = new("Item", jEntry.cItem.raw) + newItem:NormaliseQuality() + local rarityColor = colorCodes[jEntry.cItem.rarity] or colorCodes.NORMAL + local bestSlotLabel = jEntry.label .. (extra.bestSocket and " (best socket)" or "") + return { + category = "Item", + categoryColor = rarityColor, + nameColor = rarityColor, + name = (jEntry.cItem.name or "Unknown") .. ", " .. bestSlotLabel, + itemObj = newItem, + impact = impactVal, + impactStr = impactStr, + impactPercent = impactPercent, + combinedImpactStr = combinedImpactStr, + pathDist = nil, + perPoint = nil, + perPointStr = nil, + } + elseif desc.kind == "skillGem" then + local cGroup = compareEntry.skillsTab and compareEntry.skillsTab.socketGroupList[desc.groupIndex] + if not cGroup then + return nil + end + return { + category = "Skill gem", + categoryColor = colorCodes.GEM, + nameColor = colorCodes.GEM, + name = self:GetSocketGroupLabel(cGroup), + impact = impactVal, + impactStr = impactStr, + impactPercent = impactPercent, + combinedImpactStr = combinedImpactStr, + pathDist = nil, + perPoint = nil, + perPointStr = nil, + } + elseif desc.kind == "supportGem" then local cMainGroup = compareEntry.skillsTab and compareEntry.skillsTab.socketGroupList[compareEntry.mainSocketGroup] - local pMainGroup = self.primaryBuild.skillsTab and self.primaryBuild.skillsTab.socketGroupList[self.primaryBuild.mainSocketGroup] - - if cMainGroup and pMainGroup then - -- Collect support gem names already in primary build's main group - local pSupportNames = {} - for _, gem in ipairs(pMainGroup.gemList or {}) do - local ge = self:GetGemGrantedEffect(gem) - if ge and ge.support then - local name = ge.name or gem.nameSpec - if name then pSupportNames[name] = true end - end + local cGem = cMainGroup and cMainGroup.gemList and cMainGroup.gemList[desc.gemIndex] + if not cGem then + return nil + end + local cGrantedEffect = self:GetGemGrantedEffect(cGem) + local name = (cGrantedEffect and cGrantedEffect.name) or cGem.nameSpec + return { + category = "Support gem", + categoryColor = colorCodes.GEM, + nameColor = colorCodes.GEM, + name = name, + impact = impactVal, + impactStr = impactStr, + impactPercent = impactPercent, + combinedImpactStr = combinedImpactStr, + pathDist = nil, + perPoint = nil, + perPointStr = nil, + } + elseif desc.kind == "config" then + local varData + for _, vd in ipairs(self.configOptions) do + if vd.var == desc.var then + varData = vd + break end + end + if not varData then + return nil + end + local function stripColors(s) + return s:gsub("%^%x", ""):gsub("%^x%x%x%x%x%x%x", "") + end + local pVal = (self.primaryBuild.configTab.input or {})[varData.var] + local cVal = (compareEntry.configTab.input or {})[varData.var] + local displayName = varData.label or varData.var + displayName = displayName:gsub(":$", "") + local pDisplay = stripColors(self:FormatConfigValue(varData, pVal)) + local cDisplay = stripColors(self:FormatConfigValue(varData, cVal)) + return { + category = "Config", + categoryColor = colorCodes.FRACTURED, + nameColor = "^7", + name = displayName .. " (" .. pDisplay .. " -> " .. cDisplay .. ")", + impact = impactVal, + impactStr = impactStr, + impactPercent = impactPercent, + combinedImpactStr = combinedImpactStr, + pathDist = nil, + perPoint = nil, + perPointStr = nil, + } + end + return nil +end - for _, cGem in ipairs(cMainGroup.gemList or {}) do - local cGrantedEffect = self:GetGemGrantedEffect(cGem) - if cGrantedEffect and cGrantedEffect.support then - local name = cGrantedEffect.name or cGem.nameSpec - if name and not pSupportNames[name] then - -- Create a temporary copy of this support gem - local tempGem = { - nameSpec = cGem.nameSpec, - level = cGem.level, - quality = cGem.quality, - qualityId = cGem.qualityId, - enabled = cGem.enabled, - grantedEffect = cGem.grantedEffect, - gemData = cGem.gemData, - count = cGem.count, - enableGlobal1 = cGem.enableGlobal1, - enableGlobal2 = cGem.enableGlobal2, - } - - -- Temporarily add to primary build's main socket group - t_insert(pMainGroup.gemList, tempGem) - self.primaryBuild.buildFlag = true +-- Coroutine: calculate power of compared build elements against primary build +function CompareTabClass:ComparePowerBuilder(compareEntry, powerStat, categories) + local results = {} + local descriptors = self:EnumerateComparePowerCandidates(compareEntry, categories) + local total = #descriptors - local ok, sgCalcFunc, sgCalcBase = pcall(function() - return self.calcs.getMiscCalculator(self.primaryBuild) - end) + if total == 0 then + self.comparePowerResults = results + self.comparePowerProgress = 100 + return + end - -- Always remove the temporarily added gem - t_remove(pMainGroup.gemList) - self.primaryBuild.buildFlag = true + local ctx = self:MakeComparePowerContext(compareEntry, powerStat) + local fmtCtx = self:MakeComparePowerFormatContext(compareEntry, powerStat, ctx.calcBase) + local start = GetTime() - if not ok then - ConPrintf("Compare power (support gem): %s", tostring(sgCalcFunc)) - else - local impact = self.primaryBuild.calcsTab:CalculatePowerStat(powerStat, sgCalcBase, calcBase) - local impactStr, impactVal, combinedImpactStr, impactPercent, impactIsZero = formatImpact(impact) - - if not impactIsZero then - t_insert(results, { - category = "Support gem", - categoryColor = colorCodes.GEM, - nameColor = colorCodes.GEM, - name = name, - impact = impactVal, - impactStr = impactStr, - impactPercent = impactPercent, - combinedImpactStr = combinedImpactStr, - pathDist = nil, - perPoint = nil, - perPointStr = nil, - }) - end - end - processed = processed + 1 - if coroutine.running() and GetTime() - start > 100 then - self.comparePowerProgress = m_floor(processed / total * 100) - coroutine.yield() - start = GetTime() - end - end - end + for processed, desc in ipairs(descriptors) do + local ok, impact, extra = self:ComputeComparePowerCandidate(ctx, desc) + if not ok then + ConPrintf("Compare power (%s): %s", desc.kind, tostring(impact)) + elseif impact then + local row = self:BuildComparePowerRow(desc, impact, extra or {}, fmtCtx) + if row then + t_insert(results, row) end end + if coroutine.running() and GetTime() - start > 100 then + self.comparePowerProgress = m_floor(processed / total * 100) + coroutine.yield() + start = GetTime() + end end - -- ========================================== - -- Config Options - -- ========================================== - if categories.config then - local pInput = self.primaryBuild.configTab.input - local cInput = compareEntry.configTab.input or {} + self.comparePowerResults = results + self.comparePowerProgress = 100 +end - local function stripColors(s) - return s:gsub("%^%x", ""):gsub("%^x%x%x%x%x%x%x", "") +-- Try to run the compare power report on background workers; returns true if a +-- job was launched, false if the caller should use the single-core coroutine +function CompareTabClass:LaunchParallelComparePower(compareEntry) + if not ParallelRunner:IsAvailable() or not compareEntry.xmlText then + return false + end + local powerStat = self.comparePowerStat + local powerStatIndex + for i, stat in ipairs(data.powerStatList) do + if stat == powerStat then + powerStatIndex = i + break end - - for _, varData in ipairs(self.configOptions) do - if varData.var and varData.apply and varData.type ~= "text" then - local pVal = pInput[varData.var] - local cVal = cInput[varData.var] - local pNorm, cNorm = self:NormalizeConfigVals(varData, pVal, cVal) - - if pNorm ~= cNorm then - -- Save original value - local savedVal = pInput[varData.var] - - -- Apply compare build's config value - pInput[varData.var] = cVal - - -- Rebuild and calculate (pcall to guarantee restore on error) - local ok, cfgCalcFunc, cfgCalcBase = pcall(function() - self.primaryBuild.configTab:BuildModList() - self.primaryBuild.buildFlag = true - return self.calcs.getMiscCalculator(self.primaryBuild) - end) - - -- Always restore original value - pInput[varData.var] = savedVal - self.primaryBuild.configTab:BuildModList() - self.primaryBuild.buildFlag = true - - if not ok then - -- cfgCalcFunc contains the error message on failure; skip this config - ConPrintf("Compare power (config): %s", tostring(cfgCalcFunc)) - else - local impact = self.primaryBuild.calcsTab:CalculatePowerStat(powerStat, cfgCalcBase, calcBase) - local impactStr, impactVal, combinedImpactStr, impactPercent, impactIsZero = formatImpact(impact) - - -- Only include configs with non-zero impact - if not impactIsZero then - -- Build display name with value change description - local displayName = varData.label or varData.var - displayName = displayName:gsub(":$", "") - - local pDisplay = stripColors(self:FormatConfigValue(varData, pVal)) - local cDisplay = stripColors(self:FormatConfigValue(varData, cVal)) - - t_insert(results, { - category = "Config", - categoryColor = colorCodes.FRACTURED, - nameColor = "^7", - name = displayName .. " (" .. pDisplay .. " -> " .. cDisplay .. ")", - impact = impactVal, - impactStr = impactStr, - impactPercent = impactPercent, - combinedImpactStr = combinedImpactStr, - pathDist = nil, - perPoint = nil, - perPointStr = nil, - }) + end + if not powerStatIndex then + return false + end + local descriptors = self:EnumerateComparePowerCandidates(compareEntry, self.comparePowerCategories) + -- Skill gem and config candidates each cost a full recalculation, so weight + -- compare candidates higher than plain calcFunc calls + if not ParallelRunner:ShouldParallelize(#descriptors, 3) then + return false + end + local buildXML = self.primaryBuild:SaveDB("parallelCompare") + if not buildXML then + return false + end + local revision = self.primaryBuild.outputRevision + local calcFunc, calcBase = self.calcs.getMiscCalculator(self.primaryBuild) + local fmtCtx = self:MakeComparePowerFormatContext(compareEntry, powerStat, calcBase) + for i, desc in ipairs(descriptors) do + desc.i = i + end + local batches = { } + for i, slice in ipairs(ParallelRunner:PartitionList(descriptors, ParallelRunner:GetWorkerCount())) do + batches[i] = { descriptors = slice } + end + local job + job = ParallelRunner:LaunchJob({ + mode = "compare", + buildXML = buildXML, + auxText = compareEntry.xmlText, + common = { powerStatIndex = powerStatIndex }, + batches = batches, + total = #descriptors, + expectedBaseline = PowerCalcTasks.extractBaseline(calcBase, powerStat, powerStat.stat == "FullDPS"), + onProgress = function(percent) + self.comparePowerProgress = percent + end, + onComplete = function(payloads) + if self.comparePowerJob ~= job then + return + end + self.comparePowerJob = nil + if self.primaryBuild.outputRevision ~= revision or self.comparePowerCompareId ~= compareEntry then + -- The build or comparison changed while workers were running + self.comparePowerDirty = true + return + end + local resultByIndex = { } + for _, payload in pairs(payloads) do + for _, res in ipairs(payload.results or { }) do + resultByIndex[res.i] = res + end + end + local results = { } + for i, desc in ipairs(descriptors) do + local res = resultByIndex[i] + if res then + if not res.ok then + ConPrintf("Compare power (%s): %s", desc.kind, tostring(res.err)) + elseif res.impact then + local row = self:BuildComparePowerRow(desc, PowerCalcTasks.decodeNumber(res.impact), res.extra or { }, fmtCtx) + if row then + t_insert(results, row) end end - - processed = processed + 1 - if coroutine.running() and GetTime() - start > 100 then - self.comparePowerProgress = m_floor(processed / total * 100) - coroutine.yield() - start = GetTime() - end end end - end + self.comparePowerResults = results + self.comparePowerProgress = 100 + self.comparePowerListSynced = false + end, + onError = function() + if self.comparePowerJob ~= job then + return + end + self.comparePowerJob = nil + -- Fall back to the single-core coroutine path + self.comparePowerCoroutine = coroutine.create(function() + self:ComparePowerBuilder(compareEntry, self.comparePowerStat, self.comparePowerCategories) + end) + end, + }) + if not job then + return false end - - self.comparePowerResults = results - self.comparePowerProgress = 100 + self.comparePowerJob = job + return true end -- Drive the compare power report coroutine @@ -3065,9 +3141,18 @@ function CompareTabClass:RunComparePowerReport(compareEntry) self.comparePowerResults = nil self.comparePowerProgress = 0 self.comparePowerListSynced = false - self.comparePowerCoroutine = coroutine.create(function() - self:ComparePowerBuilder(compareEntry, self.comparePowerStat, self.comparePowerCategories) - end) + self.comparePowerCoroutine = nil + if self.comparePowerJob then + -- A rebuild was requested while workers were still running; their + -- results are stale, so cancel and start over + self.comparePowerJob:Cancel() + self.comparePowerJob = nil + end + if not self:LaunchParallelComparePower(compareEntry) then + self.comparePowerCoroutine = coroutine.create(function() + self:ComparePowerBuilder(compareEntry, self.comparePowerStat, self.comparePowerCategories) + end) + end end -- Resume coroutine diff --git a/src/Classes/ItemDBControl.lua b/src/Classes/ItemDBControl.lua index 3136d1a8476..7bc9efc7c5f 100644 --- a/src/Classes/ItemDBControl.lua +++ b/src/Classes/ItemDBControl.lua @@ -214,39 +214,18 @@ function ItemDBClass:BuildSortOrder() t_insert(self.sortOrder, self.sortControl.NAME) end -function ItemDBClass:ListBuilder() +function ItemDBClass:BuildFilteredList() local list = { } for id, item in pairs(self.db.list) do if self:DoesItemMatchFilters(item) then t_insert(list, item) end end + return list +end - if self.sortDetail and self.sortDetail.stat then -- stat-based - local useFullDPS = self.sortDetail.stat == "FullDPS" - local start = GetTime() - local calcFunc, calcBase = self.itemsTab.build.calcsTab:GetMiscCalculator(self.build) - for itemIndex, item in ipairs(list) do - item.measuredPower = 0 - for slotName, slot in pairs(self.itemsTab.slots) do - if self.itemsTab:IsItemValidForSlot(item, slotName) and not slot.inactive and (not slot.weaponSet or slot.weaponSet == (self.itemsTab.activeItemSet.useSecondWeaponSet and 2 or 1)) then - local output = calcFunc(item.base.flask and { toggleFlask = item } or item.base.tincture and { toggleTincture = item } or { repSlotName = slotName, repItem = item }, useFullDPS) - local measuredPower = output.Minion and output.Minion[self.sortMode] or output[self.sortMode] or 0 - if self.sortDetail.transform then - measuredPower = self.sortDetail.transform(measuredPower) - end - item.measuredPower = m_max(item.measuredPower, measuredPower) - end - end - local now = GetTime() - if now - start > 50 then - self.defaultText = "^7Sorting... ("..m_floor(itemIndex/#list*100).."%)" - coroutine.yield() - start = now - end - end - end - +-- Sort the list and publish it +function ItemDBClass:FinishList(list) table.sort(list, function(a, b) for _, data in ipairs(self.sortOrder) do local aVal = a[data.key] @@ -273,6 +252,100 @@ function ItemDBClass:ListBuilder() self.defaultText = "^7No items found that match those filters." end +function ItemDBClass:ListBuilder() + local list = self:BuildFilteredList() + + if self.sortDetail and self.sortDetail.stat then -- stat-based + local useFullDPS = self.sortDetail.stat == "FullDPS" + local start = GetTime() + local calcFunc, calcBase = self.itemsTab.build.calcsTab:GetMiscCalculator(self.build) + for itemIndex, item in ipairs(list) do + item.measuredPower = PowerCalcTasks.itemdb.measureOne(self.itemsTab, calcFunc, self.sortDetail, self.sortMode, useFullDPS, item) + local now = GetTime() + if now - start > 50 then + self.defaultText = "^7Sorting... ("..m_floor(itemIndex/#list*100).."%)" + coroutine.yield() + start = now + end + end + end + + self:FinishList(list) +end + +-- Try to run the stat sort on background workers; returns true if a job was +-- launched, false if the caller should use the single-core coroutine instead +function ItemDBClass:LaunchParallelListBuild() + if not (self.sortDetail and self.sortDetail.stat) or self.db.loading or not ParallelRunner:IsAvailable() then + return false + end + local list = self:BuildFilteredList() + -- Items are checked against every slot they fit, so weight them higher + -- than single-calculation candidates + if not ParallelRunner:ShouldParallelize(#list, 2) then + return false + end + local build = self.itemsTab.build + local buildXML = build:SaveDB("parallelSort") + if not buildXML then + return false + end + local useFullDPS = self.sortDetail.stat == "FullDPS" + local calcFunc, calcBase = build.calcsTab:GetMiscCalculator() + local names = { } + for _, item in ipairs(list) do + t_insert(names, item.name) + end + local batches = { } + for i, nameSlice in ipairs(ParallelRunner:PartitionList(names, ParallelRunner:GetWorkerCount())) do + batches[i] = { names = nameSlice } + end + local job + job = ParallelRunner:LaunchJob({ + mode = "itemdb", + buildXML = buildXML, + common = { + sortMode = self.sortDetail.sortMode, + dbType = self.dbType, + }, + batches = batches, + total = #list, + expectedBaseline = PowerCalcTasks.extractBaseline(calcBase, self.sortDetail, useFullDPS), + onProgress = function(percent) + self.defaultText = "^7Sorting... ("..percent.."%)" + end, + onComplete = function(payloads) + if self.parallelJob ~= job then + return + end + self.parallelJob = nil + local powerByName = { } + for _, payload in pairs(payloads) do + for name, power in pairs(payload.results or { }) do + powerByName[name] = PowerCalcTasks.decodeNumber(power) + end + end + for _, item in ipairs(list) do + item.measuredPower = powerByName[item.name] or 0 + end + self:FinishList(list) + end, + onError = function() + if self.parallelJob ~= job then + return + end + self.parallelJob = nil + self.listBuilder = coroutine.create(self.ListBuilder) + end, + }) + if not job then + return false + end + self.parallelJob = job + self.defaultText = "^7Sorting..." + return true +end + function ItemDBClass:Draw(viewPort) if self.itemsTab.build.outputRevision ~= self.listOutputRevision then self.listBuildFlag = true @@ -280,8 +353,17 @@ function ItemDBClass:Draw(viewPort) if self.listBuildFlag then self.listBuildFlag = false wipeTable(self.list) - self.listBuilder = coroutine.create(self.ListBuilder) + self.listBuilder = nil + if self.parallelJob then + -- A rebuild was requested while workers were still running; their + -- results are stale, so cancel and start over + self.parallelJob:Cancel() + self.parallelJob = nil + end self.listOutputRevision = self.itemsTab.build.outputRevision + if not self:LaunchParallelListBuild() then + self.listBuilder = coroutine.create(self.ListBuilder) + end end if self.listBuilder and not self.db.loading then local res, errMsg = coroutine.resume(self.listBuilder, self) diff --git a/src/Classes/NotableDBControl.lua b/src/Classes/NotableDBControl.lua index d08ec4080c9..8ae01b41611 100644 --- a/src/Classes/NotableDBControl.lua +++ b/src/Classes/NotableDBControl.lua @@ -124,41 +124,28 @@ function NotableDBClass:CalculatePowerStat(selection, original, modified) return originalValue - modifiedValue end -function NotableDBClass:ListBuilder() +function NotableDBClass:BuildFilteredList() local list = { } for id, node in pairs(self.db) do if self:DoesNotableMatchFilters(node) then t_insert(list, node) end end + return list +end - if self.sortDetail and self.sortDetail.stat then -- stat-based - local cache = { } +-- Rescale infinite results, sort the list and publish it +function NotableDBClass:FinishList(list) + if self.sortDetail and self.sortDetail.stat then local infinites = { } - local start = GetTime() - local calcFunc = self.itemsTab.build.calcsTab:GetMiscCalculator() - local itemType = self.itemsTab.displayItem.base.type - local calcBase = calcFunc({ repSlotName = itemType, repItem = self.itemsTab:anointItem(nil) }) self.sortMaxPower = 0 - for nodeIndex, node in ipairs(list) do - node.measuredPower = 0 - if node.modKey ~= "" then - local output = calcFunc({ repSlotName = itemType, repItem = self.itemsTab:anointItem(node) }) - node.measuredPower = self:CalculatePowerStat(self.sortDetail, output, calcBase) - if node.measuredPower == m_huge then - t_insert(infinites, node) - else - self.sortMaxPower = m_max(self.sortMaxPower, node.measuredPower) - end - end - local now = GetTime() - if now - start > 50 then - self.defaultText = "^7Sorting... ("..m_floor(nodeIndex/#list*100).."%)" - coroutine.yield() - start = now + for _, node in ipairs(list) do + if node.measuredPower == m_huge then + t_insert(infinites, node) + else + self.sortMaxPower = m_max(self.sortMaxPower, node.measuredPower or 0) end end - if #infinites > 0 then self.sortMaxPower = self.sortMaxPower * 2 for _, node in ipairs(infinites) do @@ -193,6 +180,103 @@ function NotableDBClass:ListBuilder() self.defaultText = "^7No notables found that match those filters." end +function NotableDBClass:ListBuilder() + local list = self:BuildFilteredList() + + if self.sortDetail and self.sortDetail.stat then -- stat-based + local start = GetTime() + local calcFunc = self.itemsTab.build.calcsTab:GetMiscCalculator() + local itemType = self.itemsTab.displayItem.base.type + local calcBase = calcFunc({ repSlotName = itemType, repItem = self.itemsTab:anointItem(nil) }) + self.sortMaxPower = 0 + for nodeIndex, node in ipairs(list) do + node.measuredPower = PowerCalcTasks.notable.measureOne(self.itemsTab, calcFunc, calcBase, self.sortDetail, itemType, node) + if node.measuredPower ~= m_huge then + self.sortMaxPower = m_max(self.sortMaxPower, node.measuredPower) + end + local now = GetTime() + if now - start > 50 then + self.defaultText = "^7Sorting... ("..m_floor(nodeIndex/#list*100).."%)" + coroutine.yield() + start = now + end + end + end + + self:FinishList(list) +end + +-- Try to run the stat sort on background workers; returns true if a job was +-- launched, false if the caller should use the single-core coroutine instead +function NotableDBClass:LaunchParallelListBuild() + if not (self.sortDetail and self.sortDetail.stat) or not ParallelRunner:IsAvailable() then + return false + end + local list = self:BuildFilteredList() + if not ParallelRunner:ShouldParallelize(#list) then + return false + end + local build = self.itemsTab.build + local buildXML = build:SaveDB("parallelSort") + if not buildXML then + return false + end + local calcFunc, calcBase = build.calcsTab:GetMiscCalculator() + local nodeIds = { } + for _, node in ipairs(list) do + t_insert(nodeIds, node.id) + end + local batches = { } + for i, ids in ipairs(ParallelRunner:PartitionList(nodeIds, ParallelRunner:GetWorkerCount())) do + batches[i] = { nodeIds = ids } + end + local job + job = ParallelRunner:LaunchJob({ + mode = "notable", + buildXML = buildXML, + auxText = self.itemsTab.displayItem:BuildRaw(), + common = { + sortMode = self.sortDetail.sortMode, + anointEnchantSlot = self.itemsTab.anointEnchantSlot or 1, + }, + batches = batches, + total = #list, + expectedBaseline = PowerCalcTasks.extractBaseline(calcBase, self.sortDetail, false), + onProgress = function(percent) + self.defaultText = "^7Sorting... ("..percent.."%)" + end, + onComplete = function(payloads) + if self.parallelJob ~= job then + return + end + self.parallelJob = nil + local powerById = { } + for _, payload in pairs(payloads) do + for idStr, power in pairs(payload.results or { }) do + powerById[tonumber(idStr)] = PowerCalcTasks.decodeNumber(power) + end + end + for _, node in ipairs(list) do + node.measuredPower = powerById[node.id] or 0 + end + self:FinishList(list) + end, + onError = function() + if self.parallelJob ~= job then + return + end + self.parallelJob = nil + self.listBuilder = coroutine.create(self.ListBuilder) + end, + }) + if not job then + return false + end + self.parallelJob = job + self.defaultText = "^7Sorting..." + return true +end + ---@param viewPort table function NotableDBClass:Draw(viewPort) if self.itemsTab.build.outputRevision ~= self.listOutputRevision then @@ -201,8 +285,17 @@ function NotableDBClass:Draw(viewPort) if self.listBuildFlag then self.listBuildFlag = false wipeTable(self.list) - self.listBuilder = coroutine.create(self.ListBuilder) + self.listBuilder = nil + if self.parallelJob then + -- A rebuild was requested while workers were still running; their + -- results are stale, so cancel and start over + self.parallelJob:Cancel() + self.parallelJob = nil + end self.listOutputRevision = self.itemsTab.build.outputRevision + if not self:LaunchParallelListBuild() then + self.listBuilder = coroutine.create(self.ListBuilder) + end end if self.listBuilder then local res, errMsg = coroutine.resume(self.listBuilder, self) diff --git a/src/Launch.lua b/src/Launch.lua index e018879cadc..c6c44c7cc9b 100644 --- a/src/Launch.lua +++ b/src/Launch.lua @@ -146,6 +146,14 @@ function launch:OnKeyDown(key, doubleClick) local before = collectgarbage("count") collectgarbage("collect") ConPrintf("%dkB => %dkB", before, collectgarbage("count")) + elseif key == "F7" and self.devMode then + -- Probe the parallel worker environment using the currently loaded build + local buildMode = self.main and self.main.modes and self.main.modes["BUILD"] + if ParallelRunner and buildMode and buildMode.spec then + ParallelRunner:RunProbe(buildMode) + else + ConPrintf("Probe: no build loaded") + end elseif key == "PAUSE" and self.devMode and profiler then if profiling then profiler.stop() @@ -207,6 +215,10 @@ function launch:OnSubCall(func, ...) end function launch:OnSubError(id, errMsg) + if not self.subScripts[id] then + -- Subscript was aborted (e.g. cancelled worker job); ignore the late event + return + end if self.subScripts[id].type == "UPDATE" then self:ShowErrMsg("In update thread: %s", errMsg) self.updateCheckRunning = false @@ -215,11 +227,22 @@ function launch:OnSubError(id, errMsg) if errMsg then self:ShowErrMsg("In download callback: %s", errMsg) end + elseif self.subScripts[id].type == "CUSTOM" then + if self.subScripts[id].callback then + local cbErrMsg = PCall(self.subScripts[id].callback, nil, errMsg) + if cbErrMsg then + self:ShowErrMsg("In subscript callback: %s", cbErrMsg) + end + end end self.subScripts[id] = nil end function launch:OnSubFinished(id, ...) + if not self.subScripts[id] then + -- Subscript was aborted (e.g. cancelled worker job); ignore the late event + return + end if self.subScripts[id].type == "UPDATE" then self.updateAvailable, self.updateErrMsg = ... self.updateCheckRunning = false diff --git a/src/Modules/Build.lua b/src/Modules/Build.lua index b7376f5d6bb..ac1ef19baed 100644 --- a/src/Modules/Build.lua +++ b/src/Modules/Build.lua @@ -915,6 +915,14 @@ function buildMode:CanExit(mode) end function buildMode:Shutdown() + -- All parallel jobs compute against this build; their results are useless now + ParallelRunner:CancelAll() + if self.calcsTab then + self.calcsTab.powerJob = nil + end + if self.compareTab then + self.compareTab.comparePowerJob = nil + end if launch.devMode and (not main.disableDevAutoSave) and self.targetVersion and not self.abortSave then if self.dbFileName then self:SaveDBFile() diff --git a/src/Modules/DataLegionLookUpTableHelper.lua b/src/Modules/DataLegionLookUpTableHelper.lua index 05b6982ff9e..4a7bbccc05d 100644 --- a/src/Modules/DataLegionLookUpTableHelper.lua +++ b/src/Modules/DataLegionLookUpTableHelper.lua @@ -50,9 +50,25 @@ local function loadJewelFile(jewelTypeName) jewelData = uncompressedFile:read("*a") uncompressedFile:close() end - if jewelData then + if jewelData and #jewelData > 0 then return jewelData end + jewelData = nil + end + + -- Headless contexts (busted tests, calculation workers) have no working + -- NewFileSearch, so file freshness can't be compared above; trust an + -- existing non-empty .bin cache, which the full application keeps fresh + if not uncompressedFileAttr.fileName and not compressedFileAttr.fileName then + local uncompressedFile = io.open(scriptPath .. jewelTypeName .. ".bin", "rb") + if uncompressedFile then + jewelData = uncompressedFile:read("*a") + uncompressedFile:close() + if jewelData and #jewelData > 0 then + return jewelData + end + jewelData = nil + end end ConPrintf("Failed to load " .. scriptPath .. jewelTypeName .. ".bin, or data is out of date, falling back to compressed file") @@ -63,10 +79,18 @@ local function loadJewelFile(jewelTypeName) elseif splitFile ~= "" then jewelData = Inflate(splitFile) end + if jewelData == "" then + -- Inflate is stubbed out in headless environments; treat as missing + -- rather than caching empty data + jewelData = nil + end if jewelData == nil then ConPrintf("Failed to load either file: " .. jewelTypeName .. ".zip, " .. jewelTypeName .. ".bin") - else + elseif compressedFileAttr.fileName or splitFile ~= "" then + -- Cache the uncompressed data for future loads. Only done when a real + -- NewFileSearch located the compressed file: headless workers and tests + -- must never write the cache shared with the full application local uncompressedFile = io.open(scriptPath .. jewelTypeName .. ".bin", "wb+") if uncompressedFile then uncompressedFile:write(jewelData) diff --git a/src/Modules/Main.lua b/src/Modules/Main.lua index beae31b7beb..9929eb565ac 100644 --- a/src/Modules/Main.lua +++ b/src/Modules/Main.lua @@ -26,6 +26,8 @@ LoadModule("Modules/BuildSiteTools") -- Load as global so other modules can access the same instance ToastNotification = LoadModule("Modules/ToastNotification") +ParallelRunner = LoadModule("Modules/ParallelRunner") +PowerCalcTasks = LoadModule("Modules/PowerCalcTasks") --[[if launch.devMode then for skillName, skill in pairs(data.enchantments.Helmet) do @@ -119,6 +121,8 @@ function main:Init() self.showFlavourText = true self.showAnimations = true self.showAllItemAffixes = true + self.computationMode = "MULTI" + self.workerCount = 0 self.errorReadingSettings = false if not SetDPIScaleOverridePercent then SetDPIScaleOverridePercent = function(scale) end end @@ -630,6 +634,12 @@ function main:LoadSettings(ignoreBuild) self.dpiScaleOverridePercent = tonumber(node.attrib.dpiScaleOverridePercent) or 0 SetDPIScaleOverridePercent(self.dpiScaleOverridePercent) end + if node.attrib.computationMode == "MULTI" or node.attrib.computationMode == "SINGLE" then + self.computationMode = node.attrib.computationMode + end + if node.attrib.workerCount then + self.workerCount = m_min(m_max(tonumber(node.attrib.workerCount) or 0, 0), 16) + end end end end @@ -762,6 +772,8 @@ function main:SaveSettings() showAnimations = tostring(self.showAnimations), showAllItemAffixes = tostring(self.showAllItemAffixes), dpiScaleOverridePercent = tostring(self.dpiScaleOverridePercent), + computationMode = self.computationMode, + workerCount = tostring(self.workerCount or 0), } }) local res, errMsg = common.xml.SaveXMLFile(setXML, self.userPath.."Settings.xml") if not res then @@ -845,11 +857,13 @@ function main:OpenOptionsPopup(savedState) showFlavourText = self.showFlavourText, showAnimations = self.showAnimations, showAllItemAffixes = self.showAllItemAffixes, - dpiScaleOverridePercent = self.dpiScaleOverridePercent + dpiScaleOverridePercent = self.dpiScaleOverridePercent, + computationMode = self.computationMode, + workerCount = self.workerCount } -- NOTE: Height needs to be adjusted if more menu options are added - local oneColumnHeightReq = 850 -- Min height required to not split menu into two columns + local oneColumnHeightReq = 880 -- Min height required to not split menu into two columns local columnWidth = 600 local startingY = 20 @@ -954,6 +968,29 @@ function main:OpenOptionsPopup(savedState) controls.nodePowerTheme.tooltipText = "Changes the colour scheme used for the node power display on the passive tree." controls.nodePowerTheme:SelByValue(self.nodePowerTheme, "theme") + nextRow() + controls.computationMode = new("DropDownControl", { "TOPLEFT", controls.sectionAnchor, "TOPLEFT" }, { currentX + defaultLabelPlacementX, currentY, 170, 18 }, { + { label = "Multi-core (recommended)", mode = "MULTI" }, + { label = "Single-core", mode = "SINGLE" }, + }, function(index, value) + self.computationMode = value.mode + if ParallelRunner then + -- Give parallel mode another chance if a previous failure disabled it + ParallelRunner.disabledThisSession = false + end + end) + controls.computationModeLabel = new("LabelControl", { "RIGHT", controls.computationMode, "LEFT" }, { defaultLabelSpacingPx, 0, 0, 16 }, "^7Computation mode:") + controls.computationMode.tooltipText = "Multi-core runs heavy calculations (tree heat map / power report, compare tab power report,\nitem and anoint list sorting) on background worker threads, using multiple CPU cores.\nEach worker uses extra memory while it runs. If results ever look wrong, switch to single-core.\nSingle-core uses the original in-process calculation." + controls.computationMode:SelByValue(self.computationMode, "mode") + controls.workerCount = new("EditControl", { "LEFT", controls.computationMode, "RIGHT" }, { 4, 0, 60, 18 }, tostring(self.workerCount or 0), nil, "%D", 2, function(buf) + self.workerCount = m_min(m_max(tonumber(buf) or 0, 0), 16) + end) + controls.workerCountLabel = new("LabelControl", { "LEFT", controls.workerCount, "RIGHT" }, { 4, 0, 0, 16 }, "^7worker threads (0 = auto)") + controls.workerCount.tooltipText = function() + local detected = tonumber(os.getenv("NUMBER_OF_PROCESSORS") or "") or 2 + return "Number of background worker threads used in multi-core mode.\n0 = automatic: one less than the number of CPU cores, up to 8.\nOn this system, automatic uses "..m_max(1, m_min(detected - 1, 8)).." workers." + end + nextRow() controls.colorPositive = new("EditControl", { "TOPLEFT", controls.sectionAnchor, "TOPLEFT" }, { currentX + defaultLabelPlacementX, currentY, 100, 18 }, tostring(self.colorPositive:gsub('^(^)', '0')), nil, nil, 8, function(buf) local match = string.match(buf, "0x%x+") @@ -1193,6 +1230,8 @@ function main:OpenOptionsPopup(savedState) self.showAllItemAffixes = savedState.showAllItemAffixes self.dpiScaleOverridePercent = savedState.dpiScaleOverridePercent SetDPIScaleOverridePercent(self.dpiScaleOverridePercent) + self.computationMode = savedState.computationMode + self.workerCount = savedState.workerCount main:ClosePopup() end) diff --git a/src/Modules/ParallelRunner.lua b/src/Modules/ParallelRunner.lua new file mode 100644 index 00000000000..bc78ae4da32 --- /dev/null +++ b/src/Modules/ParallelRunner.lua @@ -0,0 +1,361 @@ +-- Path of Building +-- +-- Module: Parallel Runner +-- Worker pool for running batched calculations on background threads. +-- Each worker is an isolated Lua VM started via the engine's LaunchSubScript; +-- work is described by a mode string plus JSON payloads, and results come back +-- as a single JSON string per worker. If a worker fails, or its results diverge +-- from the main thread's baseline, the whole job fails and the caller falls +-- back to the single-core code path. +-- +local dkjson = require "dkjson" + +local ipairs = ipairs +local pairs = pairs +local t_insert = table.insert +local m_abs = math.abs +local m_max = math.max +local m_min = math.min +local m_floor = math.floor +local s_format = string.format + +local ParallelRunner = { + jobCount = 0, + activeJobs = { }, + -- Set after a worker error or baseline mismatch so we don't repeatedly pay + -- worker bootstrap costs for a broken configuration. Cleared on restart or + -- when the user changes the computation mode in Options. + disabledThisSession = false, +} + +-- In dev mode, mirror job lifecycle events to a log file so worker problems +-- can be diagnosed without console access +local function logEvent(fmt, ...) + if not (launch and launch.devMode) then + return + end + local file = io.open(GetScriptPath().."/parallel_log.txt", "a") + if file then + file:write(os.date("%H:%M:%S"), " ", s_format(fmt, ...), "\n") + file:close() + end +end + +local jobClass = { } +jobClass.__index = jobClass + +-- Cancel all outstanding workers of this job; late finish/error events for +-- aborted subscripts are ignored (nil-guarded in launch:OnSubFinished/OnSubError) +function jobClass:Cancel() + if self.cancelled then + return + end + self.cancelled = true + for i, id in pairs(self.workers) do + AbortSubScript(id) + launch.subScripts[id] = nil + self.workers[i] = nil + end + ParallelRunner.activeJobs[self.id] = nil +end + +function ParallelRunner:IsAvailable() + -- LaunchSubScript is engine-provided; headless mode stubs it to return nil, + -- and forks without it should transparently stay on the single-core path + if type(LaunchSubScript) ~= "function" then + return false + end + if self.disabledThisSession then + return false + end + return not main or main.computationMode ~= "SINGLE" +end + +function ParallelRunner:GetWorkerCount() + local count = main and main.workerCount or 0 + if count == 0 then + -- Auto: leave one core for the UI thread + local detected = tonumber(os.getenv("NUMBER_OF_PROCESSORS") or "") or 2 + return m_max(1, m_min(detected - 1, 8)) + end + return m_max(1, m_min(count, 16)) +end + +-- Decide whether a job is big enough to be worth the per-worker bootstrap cost. +-- weight is a relative estimate of per-candidate cost (1 = one cached calcFunc call) +function ParallelRunner:ShouldParallelize(candidateCount, weight) + if not self:IsAvailable() then + return false + end + return candidateCount * (weight or 1) >= 300 +end + +-- Split a flat list into up to numBatches round-robin slices +function ParallelRunner:PartitionList(list, numBatches) + local batches = { } + local n = m_min(numBatches, #list) + for i = 1, n do + batches[i] = { } + end + for i, item in ipairs(list) do + t_insert(batches[((i - 1) % n) + 1], item) + end + return batches +end + +-- Compare the baseline stats computed by a worker against the main thread's. +-- Any disagreement means the build didn't survive the XML round trip intact, +-- so per-candidate results can't be trusted. Returns a description of the +-- first mismatch, or nil if everything agrees. +local function validateBaseline(got, expected) + if type(got) ~= "table" then + return "no baseline returned" + end + for stat, value in pairs(expected) do + local workerValue = got[stat] + if type(workerValue) ~= "number" then + return s_format("stat %s missing", stat) + end + if m_abs(workerValue - value) > m_max(1e-6, 1e-3 * m_max(m_abs(workerValue), m_abs(value))) then + return s_format("stat %s: worker %g vs main %g", stat, workerValue, value) + end + end +end + +-- Launch a job: +-- desc = { +-- mode = "probe"/"nodePower"/"compare"/"notable"/"itemdb", +-- buildXML = , +-- auxText = , +-- common = , +-- batches = , +-- total = , +-- expectedBaseline = number, validated against each worker>, +-- onProgress = function(percent), +-- onComplete = function(payloads), -- array of decoded worker payloads +-- onError = function(errMsg), -- called at most once; caller should fall back +-- } +-- Returns the job (with job:Cancel()), or nil + error message if workers +-- could not be launched at all (caller should fall back synchronously). +function ParallelRunner:LaunchJob(desc) + local scriptText, errMsg = self:GetWorkerScript() + if not scriptText then + return nil, errMsg + end + self.jobCount = self.jobCount + 1 + local job = setmetatable({ + id = self.jobCount, + desc = desc, + workers = { }, + payloads = { }, + progress = { }, + pending = 0, + lastPercent = -1, + }, jobClass) + for i, batch in ipairs(desc.batches) do + local specJSON = dkjson.encode({ common = desc.common, batch = batch }) + local id = LaunchSubScript(scriptText, + "GetScriptPath,GetRuntimePath,GetWorkDir,MakeDir,GetUserPath", + "ConPrintf,ParallelWorkerProgress", + job.id, i, GetScriptPath(), desc.mode, desc.buildXML or "", desc.auxText or "", specJSON) + if not id then + job:Cancel() + return nil, "Unable to launch background worker" + end + job.workers[i] = id + job.pending = job.pending + 1 + launch:RegisterSubScript(id, function(resultJSON, workerErrMsg) + self:OnWorkerDone(job, i, resultJSON, workerErrMsg) + end) + end + if job.pending == 0 then + return nil, "No work to distribute" + end + self.activeJobs[job.id] = job + logEvent("job %d launched: mode=%s workers=%d total=%s buildXML=%dB", job.id, desc.mode, job.pending, tostring(desc.total), #(desc.buildXML or "")) + return job +end + +function ParallelRunner:OnWorkerDone(job, workerIdx, resultJSON, errMsg) + if job.cancelled or job.failed then + return + end + job.workers[workerIdx] = nil + if errMsg or not resultJSON then + self:FailJob(job, errMsg or "worker returned no result") + return + end + local payload, _, decodeErr = dkjson.decode(resultJSON) + if type(payload) ~= "table" then + self:FailJob(job, "malformed worker result: "..tostring(decodeErr)) + return + end + if payload.error then + self:FailJob(job, payload.error, payload) + return + end + if job.desc.expectedBaseline then + local mismatch = validateBaseline(payload.baseline, job.desc.expectedBaseline) + if mismatch then + self:FailJob(job, "worker results diverged from main thread ("..mismatch..")") + return + end + end + job.payloads[workerIdx] = payload + job.pending = job.pending - 1 + logEvent("job %d worker %d done: %dB result, %d pending", job.id, workerIdx, #resultJSON, job.pending) + if job.pending == 0 then + self.activeJobs[job.id] = nil + if job.desc.onComplete then + logEvent("job %d complete, merging", job.id) + job.desc.onComplete(job.payloads) + end + end +end + +function ParallelRunner:FailJob(job, errMsg, payload) + job.failed = true + job:Cancel() + self.disabledThisSession = true + ConPrintf("Parallel computation failed, falling back to single-core: %s", tostring(errMsg)) + logEvent("job %d FAILED: %s", job.id, tostring(errMsg)) + if job.desc.onError then + job.desc.onError(errMsg, payload) + end +end + +function ParallelRunner:CancelAll() + for _, job in pairs(self.activeJobs) do + job:Cancel() + end +end + +-- Public wrapper so call sites can add their own entries to the dev log +function ParallelRunner:Log(fmt, ...) + logEvent(fmt, ...) +end + +function ParallelRunner:GetWorkerScript() + if not self.workerScript then + local file = io.open("CalcWorker.lua", "r") + if not file then + file = io.open(GetScriptPath().."/CalcWorker.lua", "r") + end + if not file then + return nil, "CalcWorker.lua not found" + end + self.workerScript = file:read("*a") + file:close() + end + return self.workerScript +end + +-- Dev-mode probe: launches a single worker that bootstraps a headless program, +-- reloads the given build from XML, and reports facts about the worker VM +-- (injected functions, timings, baseline agreement). Results go to the console +-- and to probe_report.txt next to the scripts. +function ParallelRunner:RunProbe(build) + local reportLines = { } + local function report(fmt, ...) + local line = s_format(fmt, ...) + ConPrintf("%s", line) + t_insert(reportLines, line) + end + local function writeReport() + local file = io.open(GetScriptPath().."/probe_report.txt", "w") + if file then + file:write(table.concat(reportLines, "\n"), "\n") + file:close() + end + end + local buildXML = build:SaveDB("probe") + if not buildXML then + report("Probe: unable to serialize build") + writeReport() + return + end + local expectedBaseline + if build.calcsTab and build.calcsTab.miscCalculator then + local calcFunc, calcBase = build.calcsTab:GetMiscCalculator() + expectedBaseline = PowerCalcTasks.extractBaseline(calcBase, nil, false) + end + local startTime = GetTime() + report("Probe: launching worker (build XML: %d bytes)...", #buildXML) + local job, errMsg = self:LaunchJob({ + mode = "probe", + buildXML = buildXML, + common = { }, + batches = { { } }, + total = 1, + -- Baselines are compared manually below so the probe can report every + -- stat instead of failing on the first mismatch + onComplete = function(payloads) + local payload = payloads[1] + report("Probe: completed in %d ms (wall)", GetTime() - startTime) + for key, value in pairs(payload.probe or { }) do + report("Probe: %s = %s", key, tostring(value)) + end + for key, value in pairs(payload.timing or { }) do + report("Probe: timing.%s = %s", key, tostring(value)) + end + local mismatches = 0 + for stat, value in pairs(expectedBaseline or { }) do + local workerValue = payload.baseline and payload.baseline[stat] + if type(workerValue) ~= "number" then + report("Probe: BASELINE %s: main %.6g, worker MISSING", stat, value) + mismatches = mismatches + 1 + elseif m_abs(workerValue - value) > m_max(1e-6, 1e-3 * m_max(m_abs(workerValue), m_abs(value))) then + report("Probe: BASELINE %s: main %.6g, worker %.6g (%+.3f%%)", stat, value, workerValue, (workerValue - value) / value * 100) + mismatches = mismatches + 1 + else + report("Probe: baseline %s: main %.6g, worker %.6g OK", stat, value, workerValue) + end + end + if mismatches == 0 then + report("Probe: baseline validated OK against main thread") + -- A successful probe proves the environment works; re-enable if a + -- previous failure had disabled parallel mode + self.disabledThisSession = false + else + report("Probe: %d baseline stats DIVERGED", mismatches) + end + writeReport() + end, + onError = function(probeErrMsg, payload) + report("Probe: FAILED: %s", tostring(probeErrMsg)) + if payload and payload.probe then + for key, value in pairs(payload.probe) do + report("Probe: env.%s = %s", key, tostring(value)) + end + end + writeReport() + end, + }) + if not job then + report("Probe: could not launch worker: %s", tostring(errMsg)) + writeReport() + end +end + +-- Called from worker threads via launch:OnSubCall; aggregates per-worker +-- progress counters into one percentage for the job's progress callback +function ParallelWorkerProgress(jobId, workerIdx, done, total) + local job = ParallelRunner.activeJobs[jobId] + if not job or job.cancelled or job.failed then + return + end + job.progress[workerIdx] = done + if job.desc.onProgress and job.desc.total and job.desc.total > 0 then + local doneSum = 0 + for _, workerDone in pairs(job.progress) do + doneSum = doneSum + workerDone + end + local percent = m_min(m_floor(doneSum / job.desc.total * 100), 100) + if percent ~= job.lastPercent then + job.lastPercent = percent + job.desc.onProgress(percent) + end + end +end + +return ParallelRunner diff --git a/src/Modules/PowerCalcTasks.lua b/src/Modules/PowerCalcTasks.lua new file mode 100644 index 00000000000..3175332a050 --- /dev/null +++ b/src/Modules/PowerCalcTasks.lua @@ -0,0 +1,341 @@ +-- Path of Building +-- +-- Module: Power Calc Tasks +-- Shared per-candidate calculation logic for the parallelizable "iterate over +-- many candidates and rank them" features (node power / compare / anoint +-- notables / item DB sorting). Loaded both by the main thread (single-core code +-- paths and result merging) and by CalcWorker.lua inside worker VMs, so the two +-- paths always run identical calculation code. +-- +local ipairs = ipairs +local pairs = pairs +local type = type +local t_insert = table.insert +local m_huge = math.huge +local m_max = math.max + +local tasks = { } + +local function isFiniteNumber(value) + -- NaN compares unequal to itself; math.huge can't survive a JSON round trip + return type(value) == "number" and value == value and value ~= m_huge and value ~= -m_huge +end + +-- Baseline stats used to validate that a worker's reconstructed build produces +-- the same numbers as the main thread's live build. Mirrors the stats consumed +-- by CalcsTab:CalculateCombinedOffDefStat plus the active power stat. +local baselineStatList = { + "LifeUnreserved", "Life", "Armour", "EnergyShield", "EnergyShieldRecoveryCap", + "Evasion", "LifeRegenRecovery", "EnergyShieldRegenRecovery", "CombinedDPS", +} + +function tasks.extractBaseline(output, powerStat, useFullDPS) + local baseline = { } + if type(output) ~= "table" then + return baseline + end + for _, stat in ipairs(baselineStatList) do + if isFiniteNumber(output[stat]) then + baseline[stat] = output[stat] + end + end + if type(output.Minion) == "table" and isFiniteNumber(output.Minion.CombinedDPS) then + baseline["Minion.CombinedDPS"] = output.Minion.CombinedDPS + end + if useFullDPS and isFiniteNumber(output.FullDPS) then + baseline["FullDPS"] = output.FullDPS + end + if powerStat and powerStat.stat and isFiniteNumber(output[powerStat.stat]) then + baseline["powerStat."..powerStat.stat] = output[powerStat.stat] + end + return baseline +end + +-- JSON-safe number encoding: math.huge/-math.huge/NaN can't cross the JSON +-- boundary, so they're carried as sentinel strings +function tasks.encodeNumber(value) + if type(value) ~= "number" then + return nil + end + if value ~= value then + return 0 + elseif value == m_huge then + return "inf" + elseif value == -m_huge then + return "-inf" + end + return value +end + +function tasks.decodeNumber(value) + if value == "inf" then + return m_huge + elseif value == "-inf" then + return -m_huge + end + return tonumber(value) +end + +-- Resolve a DB sort drop-list selection (built from data.powerStatList by +-- NotableDBControl/ItemDBControl:BuildSortOrder) from its sortMode key; used to +-- reconstruct the same selection inside a worker VM +function tasks.findSortDetail(sortMode) + for _, stat in ipairs(data.powerStatList) do + if (stat.itemField or stat.stat) == sortMode then + return { + sortMode = sortMode, + itemField = stat.itemField, + stat = stat.stat, + transform = stat.transform, + } + end + end +end + +------------------------------------------------------------------------------ +-- Node power (heat map / power report) +------------------------------------------------------------------------------ + +tasks.nodePower = { } + +-- Worker side: run PowerBuilder for the given subset of nodes/cluster notables +-- and serialize the node.power results +function tasks.nodePower.computeBatch(build, common, batch, auxText, progressFunc) + local calcsTab = build.calcsTab + calcsTab.powerStat = common.powerStatIndex and data.powerStatList[common.powerStatIndex] or nil + calcsTab.nodePowerMaxDepth = common.nodePowerMaxDepth + local filter = { nodes = { }, cluster = { } } + for _, nodeId in ipairs(batch.nodeIds or { }) do + filter.nodes[nodeId] = true + end + for _, name in ipairs(batch.clusterNames or { }) do + filter.cluster[name] = true + end + -- Overwrite this spec's path/depends data with the main thread's; a spec + -- rebuilt from XML resolves equal-length shortest-path ties differently, + -- which would change pathPower for nodes with multiple viable paths + for idStr, ids in pairs(batch.paths or { }) do + local node = build.spec.nodes[tonumber(idStr)] + if node then + local path = { } + for _, pathId in ipairs(ids) do + local pathNode = build.spec.nodes[pathId] + if pathNode then + t_insert(path, pathNode) + end + end + if #path > 0 then + node.path = path + node.pathDist = #path + end + end + end + for idStr, ids in pairs(batch.depends or { }) do + local node = build.spec.nodes[tonumber(idStr)] + if node then + local depends = { } + for _, depId in ipairs(ids) do + local depNode = build.spec.nodes[depId] + if depNode then + t_insert(depends, depNode) + end + end + node.depends = depends + end + end + calcsTab.workerProgressFunc = progressFunc + local newPowerMax = calcsTab:PowerBuilder(filter) + calcsTab.workerProgressFunc = nil + + local encodeNumber = tasks.encodeNumber + local results = { nodes = { }, cluster = { }, powerMax = { } } + for key, value in pairs(newPowerMax or { }) do + results.powerMax[key] = encodeNumber(value) + end + for nodeId in pairs(filter.nodes) do + local node = build.spec.nodes[nodeId] + if node and node.power then + local out = { + singleStat = encodeNumber(node.power.singleStat), + pathPower = encodeNumber(node.power.pathPower), + offence = encodeNumber(node.power.offence), + defence = encodeNumber(node.power.defence), + } + if node.power.masteryEffects then + out.masteryEffects = { } + for effectId, effect in pairs(node.power.masteryEffects) do + out.masteryEffects[tostring(effectId)] = { + singleStat = encodeNumber(effect.singleStat), + pathPower = encodeNumber(effect.pathPower), + offence = encodeNumber(effect.offence), + defence = encodeNumber(effect.defence), + } + end + end + results.nodes[tostring(nodeId)] = out + end + end + for name in pairs(filter.cluster) do + local node = build.spec.tree.clusterNodeMap[name] + if node and node.power then + results.cluster[name] = { singleStat = encodeNumber(node.power.singleStat) } + end + end + + local powerStat = calcsTab.powerStat + local useFullDPS = powerStat and powerStat.stat == "FullDPS" + local calcFunc, calcBase = calcsTab:GetMiscCalculator() + return { + results = results, + baseline = tasks.extractBaseline(calcBase, powerStat, useFullDPS), + } +end + +------------------------------------------------------------------------------ +-- Anoint notable sorting (NotableDBControl) +------------------------------------------------------------------------------ + +tasks.notable = { } + +-- Measure the impact of anointing one notable onto the display item. +-- Identical to the original NotableDBControl:ListBuilder loop body. +function tasks.notable.measureOne(itemsTab, calcFunc, calcBase, sortDetail, itemType, node) + if node.modKey == "" then + return 0 + end + local output = calcFunc({ repSlotName = itemType, repItem = itemsTab:anointItem(node) }) + local original, modified = output, calcBase + if modified.Minion then + original = original.Minion + modified = modified.Minion + end + local originalValue = original[sortDetail.stat] or 0 + local modifiedValue = modified[sortDetail.stat] or 0 + if sortDetail.transform then + originalValue = sortDetail.transform(originalValue) + modifiedValue = sortDetail.transform(modifiedValue) + end + return originalValue - modifiedValue +end + +-- Worker side: auxText is the display item's raw text +function tasks.notable.computeBatch(build, common, batch, auxText, progressFunc) + local itemsTab = build.itemsTab + itemsTab.displayItem = new("Item", auxText) + itemsTab.anointEnchantSlot = common.anointEnchantSlot or 1 + local sortDetail = tasks.findSortDetail(common.sortMode) + if not sortDetail or not sortDetail.stat then + error("notable: unknown sort mode "..tostring(common.sortMode)) + end + local calcFunc, miscCalcBase = build.calcsTab:GetMiscCalculator() + local itemType = itemsTab.displayItem.base.type + local calcBase = calcFunc({ repSlotName = itemType, repItem = itemsTab:anointItem(nil) }) + local results = { } + local nodeIds = batch.nodeIds or { } + for index, nodeId in ipairs(nodeIds) do + local node = build.spec.tree.nodes[nodeId] + if node then + results[tostring(nodeId)] = tasks.encodeNumber(tasks.notable.measureOne(itemsTab, calcFunc, calcBase, sortDetail, itemType, node)) + end + if progressFunc and index % 10 == 0 then + progressFunc(index, #nodeIds) + end + end + return { + results = results, + baseline = tasks.extractBaseline(miscCalcBase, sortDetail, false), + } +end + +------------------------------------------------------------------------------ +-- Compare tab power report (CompareTab) +------------------------------------------------------------------------------ + +tasks.compare = { } + +-- Worker side: auxText is the comparison build's XML; descriptors identify +-- candidates by stable ids and are computed via the same CompareTab methods +-- the single-core path uses +function tasks.compare.computeBatch(build, common, batch, auxText, progressFunc) + local compareTab = build.compareTab + if not compareTab then + error("compare: build has no compare tab") + end + local powerStat = data.powerStatList[common.powerStatIndex] + if not powerStat then + error("compare: unknown power stat index "..tostring(common.powerStatIndex)) + end + local compareEntry = new("CompareEntry", auxText, "CalcWorker comparison") + local ctx = compareTab:MakeComparePowerContext(compareEntry, powerStat) + local results = { } + local descriptors = batch.descriptors or { } + for index, desc in ipairs(descriptors) do + local ok, impact, extra = compareTab:ComputeComparePowerCandidate(ctx, desc) + t_insert(results, { + i = desc.i, + ok = ok and true or false, + impact = ok and impact and tasks.encodeNumber(impact) or nil, + err = not ok and tostring(impact) or nil, + extra = extra, + }) + if progressFunc and index % 5 == 0 then + progressFunc(index, #descriptors) + end + end + return { + results = results, + baseline = tasks.extractBaseline(ctx.calcBase, powerStat, ctx.useFullDPS), + } +end + +------------------------------------------------------------------------------ +-- Item DB sorting (ItemDBControl) +------------------------------------------------------------------------------ + +tasks.itemdb = { } + +-- Measure an item's best impact across all slots it fits. +-- Identical to the original ItemDBControl:ListBuilder loop body. +function tasks.itemdb.measureOne(itemsTab, calcFunc, sortDetail, sortMode, useFullDPS, item) + local measuredPower = 0 + for slotName, slot in pairs(itemsTab.slots) do + if itemsTab:IsItemValidForSlot(item, slotName) and not slot.inactive and (not slot.weaponSet or slot.weaponSet == (itemsTab.activeItemSet.useSecondWeaponSet and 2 or 1)) then + local output = calcFunc(item.base.flask and { toggleFlask = item } or item.base.tincture and { toggleTincture = item } or { repSlotName = slotName, repItem = item }, useFullDPS) + local power = output.Minion and output.Minion[sortMode] or output[sortMode] or 0 + if sortDetail.transform then + power = sortDetail.transform(power) + end + measuredPower = m_max(measuredPower, power) + end + end + return measuredPower +end + +-- Worker side: candidates are item names in the shared unique/rare DB +function tasks.itemdb.computeBatch(build, common, batch, auxText, progressFunc) + local itemsTab = build.itemsTab + local sortDetail = tasks.findSortDetail(common.sortMode) + if not sortDetail or not sortDetail.stat then + error("itemdb: unknown sort mode "..tostring(common.sortMode)) + end + local useFullDPS = sortDetail.stat == "FullDPS" + local db = common.dbType == "RARE" and main.rareDB or main.uniqueDB + local calcFunc, calcBase = build.calcsTab:GetMiscCalculator() + local results = { } + local names = batch.names or { } + for index, name in ipairs(names) do + local item = db.list[name] + if item then + results[name] = tasks.encodeNumber(tasks.itemdb.measureOne(itemsTab, calcFunc, sortDetail, sortDetail.sortMode, useFullDPS, item)) + end + if progressFunc and index % 10 == 0 then + progressFunc(index, #names) + end + end + return { + results = results, + baseline = tasks.extractBaseline(calcBase, sortDetail, useFullDPS), + } +end + +return tasks diff --git a/src/parallel_log.txt b/src/parallel_log.txt new file mode 100644 index 00000000000..54ad0c28cce --- /dev/null +++ b/src/parallel_log.txt @@ -0,0 +1,103 @@ +20:26:05 job 1 launched: mode=nodePower workers=8 total=4348 buildXML=56056B +20:26:17 job 1 worker 8 done: 29479B result, 7 pending +20:26:17 job 1 worker 2 done: 30721B result, 6 pending +20:26:18 job 1 worker 4 done: 28443B result, 5 pending +20:26:18 job 1 worker 5 done: 29249B result, 4 pending +20:26:19 job 1 worker 6 done: 29317B result, 3 pending +20:26:19 job 1 worker 7 done: 31172B result, 2 pending +20:26:20 job 1 worker 1 done: 29046B result, 1 pending +20:26:21 job 1 worker 3 done: 31851B result, 0 pending +20:26:21 job 1 complete, merging +20:26:21 merge applied: 2671 node results (2552 with singleStat), powerMax.singleStat=0 +20:28:31 job 2 launched: mode=nodePower workers=8 total=4348 buildXML=56056B +20:28:45 job 2 worker 2 done: 21231B result, 7 pending +20:28:46 job 2 worker 7 done: 20830B result, 6 pending +20:28:47 job 2 worker 8 done: 20891B result, 5 pending +20:28:47 job 2 worker 1 done: 20824B result, 4 pending +20:28:48 job 2 worker 3 done: 21252B result, 3 pending +20:28:48 job 2 worker 4 done: 21363B result, 2 pending +20:28:48 job 2 worker 5 done: 20719B result, 1 pending +20:28:49 job 2 worker 6 done: 21110B result, 0 pending +20:28:49 job 2 complete, merging +20:28:49 merge applied: 2671 node results (2671 with singleStat), powerMax.singleStat=0 +20:28:51 job 3 launched: mode=nodePower workers=8 total=4348 buildXML=56462B +20:29:11 job 3 worker 5 done: 26456B result, 7 pending +20:29:12 job 3 worker 7 done: 26833B result, 6 pending +20:29:13 job 3 worker 8 done: 26669B result, 5 pending +20:29:14 job 3 worker 1 done: 26444B result, 4 pending +20:29:14 job 3 worker 2 done: 28394B result, 3 pending +20:29:15 job 3 worker 3 done: 28048B result, 2 pending +20:29:16 job 3 worker 4 done: 27750B result, 1 pending +20:29:16 job 3 worker 6 done: 26920B result, 0 pending +20:29:16 job 3 complete, merging +20:29:16 merge applied: 2671 node results (2671 with singleStat), powerMax.singleStat=1811177.1023715 +20:30:35 job 4 launched: mode=nodePower workers=8 total=4348 buildXML=56473B +20:30:36 job 5 launched: mode=nodePower workers=8 total=4348 buildXML=56462B +20:30:55 job 5 worker 3 done: 27915B result, 7 pending +20:30:56 job 5 worker 2 done: 28277B result, 6 pending +20:30:57 job 5 worker 4 done: 27617B result, 5 pending +20:30:57 job 5 worker 5 done: 26405B result, 4 pending +20:30:58 job 5 worker 6 done: 26920B result, 3 pending +20:30:59 job 5 worker 7 done: 26719B result, 2 pending +20:30:59 job 5 worker 8 done: 26641B result, 1 pending +20:31:00 job 5 worker 1 done: 26335B result, 0 pending +20:31:00 job 5 complete, merging +20:31:00 merge applied: 2671 node results (2671 with singleStat), powerMax.singleStat=1811177.1023715 +20:33:05 job 6 launched: mode=nodePower workers=8 total=4348 buildXML=56473B +20:33:12 job 7 launched: mode=nodePower workers=8 total=4348 buildXML=56462B +20:33:15 job 8 launched: mode=nodePower workers=8 total=4348 buildXML=56473B +20:33:18 job 9 launched: mode=nodePower workers=8 total=4348 buildXML=56462B +20:33:37 job 9 worker 3 done: 27926B result, 7 pending +20:33:38 job 9 worker 8 done: 26664B result, 6 pending +20:33:39 job 9 worker 1 done: 26335B result, 5 pending +20:33:40 job 9 worker 2 done: 28266B result, 4 pending +20:33:41 job 9 worker 4 done: 27617B result, 3 pending +20:33:41 job 9 worker 5 done: 26405B result, 2 pending +20:33:42 job 9 worker 6 done: 26920B result, 1 pending +20:33:43 job 9 worker 7 done: 26719B result, 0 pending +20:33:43 job 9 complete, merging +20:33:43 merge applied: 2671 node results (2671 with singleStat), powerMax.singleStat=1811177.1023715 +23:41:17 job 1 launched: mode=nodePower workers=12 total=4348 buildXML=56462B +23:41:20 job 2 launched: mode=nodePower workers=12 total=4348 buildXML=56462B +23:41:40 job 2 worker 10 done: 18596B result, 11 pending +23:41:41 job 2 worker 7 done: 17987B result, 10 pending +23:41:41 job 2 worker 8 done: 17968B result, 9 pending +23:41:42 job 2 worker 11 done: 16783B result, 8 pending +23:41:42 job 2 worker 12 done: 18085B result, 7 pending +23:41:43 job 2 worker 1 done: 18078B result, 6 pending +23:41:43 job 2 worker 2 done: 19757B result, 5 pending +23:41:44 job 2 worker 3 done: 18742B result, 4 pending +23:41:44 job 2 worker 4 done: 18747B result, 3 pending +23:41:44 job 2 worker 5 done: 17663B result, 2 pending +23:41:45 job 2 worker 6 done: 18979B result, 1 pending +23:41:45 job 2 worker 9 done: 17406B result, 0 pending +23:41:45 job 2 complete, merging +23:41:45 merge applied: 2671 node results (2671 with singleStat), powerMax.singleStat=1811177.1023715 +23:41:46 job 3 launched: mode=nodePower workers=12 total=4348 buildXML=56473B +23:41:53 job 4 launched: mode=nodePower workers=12 total=4348 buildXML=56462B +23:42:17 job 5 launched: mode=nodePower workers=12 total=4348 buildXML=56473B +23:42:36 job 5 worker 6 done: 18147B result, 11 pending +23:42:36 job 5 worker 8 done: 18167B result, 10 pending +23:42:37 job 5 worker 9 done: 17909B result, 9 pending +23:42:37 job 5 worker 10 done: 18073B result, 8 pending +23:42:38 job 5 worker 11 done: 18015B result, 7 pending +23:42:38 job 5 worker 12 done: 18556B result, 6 pending +23:42:39 job 5 worker 1 done: 18184B result, 5 pending +23:42:39 job 5 worker 2 done: 19436B result, 4 pending +23:42:40 job 5 worker 3 done: 18359B result, 3 pending +23:42:41 job 5 worker 4 done: 17919B result, 2 pending +23:42:41 job 5 worker 5 done: 17225B result, 1 pending +23:42:41 job 5 worker 7 done: 18901B result, 0 pending +23:42:41 job 5 complete, merging +23:42:41 merge applied: 2671 node results (2671 with singleStat), powerMax.singleStat=1825249.3469207 +23:42:52 job 6 launched: mode=nodePower workers=8 total=4348 buildXML=56461B +23:43:09 job 6 worker 5 done: 26643B result, 7 pending +23:43:11 job 6 worker 6 done: 26614B result, 6 pending +23:43:12 job 6 worker 7 done: 25371B result, 5 pending +23:43:13 job 6 worker 1 done: 26736B result, 4 pending +23:43:13 job 6 worker 2 done: 28473B result, 3 pending +23:43:14 job 6 worker 3 done: 28836B result, 2 pending +23:43:14 job 6 worker 4 done: 26997B result, 1 pending +23:43:15 job 6 worker 8 done: 27151B result, 0 pending +23:43:15 job 6 complete, merging +23:43:15 merge applied: 2671 node results (2671 with singleStat), powerMax.singleStat=1811177.1023715 diff --git a/src/probe_report.txt b/src/probe_report.txt new file mode 100644 index 00000000000..b223315d273 --- /dev/null +++ b/src/probe_report.txt @@ -0,0 +1,37 @@ +Probe: launching worker (build XML: 56056 bytes)... +Probe: completed in 7611 ms (wall) +Probe: buildLoaded = true +Probe: scriptPathArg = C:/Users/Albi/Desktop/PoBMC/src +Probe: singleCalcMs = 13 +Probe: specJSONBytes = 24 +Probe: buildXMLBytes = 56056 +Probe: treeOpenAbsolute = true +Probe: treeOpenRelative = true +Probe: hasIo = true +Probe: hasLoadfile = true +Probe: injectedConPrintf = true +Probe: injectedGetRuntimePath = true +Probe: injectedGetScriptPath = true +Probe: injectedGetUserPath = true +Probe: injectedGetWorkDir = true +Probe: buildName = CalcWorker +Probe: injectedNewFileSearch = false +Probe: jitVersion = LuaJIT 2.1.1753364724 +Probe: luaVersion = Lua 5.1 +Probe: packagePath = .\?.lua;C:\Users\Albi\Desktop\PoBMC\runtime\lua\?.lua;C:\Users\Albi\Desktop\PoBMC\runtime\lua\?\init.lua; +Probe: workDir = C:/Users/Albi/Desktop/PoBMC/src +Probe: injectedInflate = false +Probe: timing.calcMs = 13 +Probe: timing.bootstrapMs = 7592 +Probe: timing.totalMs = 7605 +Probe: timing.initMs = 660.00000000006 +Probe: baseline EnergyShieldRecoveryCap: main 0, worker 0 OK +Probe: baseline LifeUnreserved: main 4590, worker 4590 OK +Probe: baseline Life: main 4634, worker 4634 OK +Probe: baseline CombinedDPS: main 1.78378e+07, worker 1.78378e+07 OK +Probe: baseline Armour: main 31853, worker 31853 OK +Probe: baseline LifeRegenRecovery: main 352.2, worker 352.2 OK +Probe: baseline EnergyShieldRegenRecovery: main 10, worker 10 OK +Probe: baseline Evasion: main 16811, worker 16811 OK +Probe: baseline EnergyShield: main 0, worker 0 OK +Probe: baseline validated OK against main thread