perf: hak-audit — spredte timere, fælles cache (markør->station, salgssteder, identify, Wait-punkter, lagre, positioner), bygningsliste 10 s, profiler, simulering (v1.28.0.0)

Audit (2 reviewere) + simulering bench/bench_hak.lua: værste frame 124,7 ms -> 0,9 ms.
- Holdende vognes genplanlægning (5 s) spredes tilfældigt over hele intervallet (før: alle i samme frame).
- ADCache: korttids-cache m. spredt udløb (+0-50 %), kun aktiv i spillet; stationsopslag 30 s.
- Bygningsliste 10 s (før 1 s: bygninger x markører hvert sekund). Markarbejde-tick forskudt 0,5 s fra menuen.
- ADProfiler: logger top-6 langsomste indgange pr. minut ved > 2 ms.
This commit is contained in:
2026-09-26 11:24:05 +02:00
parent 8a18187103
commit 6fd917caca
16 changed files with 292 additions and 20 deletions
+3 -1
View File
@@ -10,7 +10,8 @@ ADBuildings.LOG_PREFIX = "ADSmartPickup: "
ADBuildings.MAX_MARKER_DISTANCE = 40
-- en separat drop-markør (≠ bygningens læssemarkør) må højst stå så langt fra aflæsningstriggeren
ADBuildings.MAX_DROP_DISTANCE = 25
ADBuildings.CACHE_MS = 1000
-- bygninger ændrer sig sjældent; genopbygningen (bygninger x markører) er dyr — 1 s gav et hak hvert sekund
ADBuildings.CACHE_MS = 10000
local cache = {list = nil, farmId = nil, time = -math.huge}
@@ -363,6 +364,7 @@ local function build(adEnv, farmId)
end
function ADBuildings.invalidate()
if ADCache ~= nil then ADCache.clear() end
cache.list, cache.time = nil, -math.huge
end
+53
View File
@@ -0,0 +1,53 @@
-- AD Cache
-- Fælles korttids-cache for dyre opslag (markør -> station, salgssteder, identify, Wait-punkter, lagre).
-- Holdende vogne genplanlægger hvert 5. s og slog de samme ting op igen og igen (markører x stationer x
-- triggere) -> hak (in-game 26/9). Verden ændrer sig ikke så hurtigt. Uden spillets tid (tests) caches
-- intet, så adfærden er som før. "Intet fundet" gemmes også.
ADCache = {}
ADCache.buckets = {}
-- tændes af ADSmartPickup når hooksene er installeret (i spillet); tests kører uden cache
ADCache.enabled = false
local NOTHING = {}
-- Til moduler der indlæses uden adCache (tests): kald fn direkte.
function ADCacheGet(bucket, key, ttlMs, fn)
if ADCache == nil or not ADCache.enabled then return fn() end
return ADCache.get(bucket, key, ttlMs, fn)
end
local function now()
local mission = g_currentMission
if mission ~= nil and type(mission.time) == "number" then return mission.time end
return nil
end
-- Resultatet af fn() for (bucket, key), genbrugt i ttlMs.
function ADCache.get(bucket, key, ttlMs, fn)
local time = now()
if not ADCache.enabled or time == nil or key == nil then return fn() end
local store = ADCache.buckets[bucket]
if store == nil then
store = {size = 0, entries = {}}
ADCache.buckets[bucket] = store
end
local entry = store.entries[key]
if entry ~= nil and time - entry.at < ttlMs then
if entry.value == NOTHING then return nil end
return entry.value
end
local value = fn()
if entry == nil then
store.size = store.size + 1
if store.size > 5000 then store.entries, store.size = {}, 1 end
end
-- spredt udløb (levetid +0-50 %): poster der blev lavet i samme kald, udløber ikke i samme frame
-- (simulering 26/9: 600 markør-opslag udløb samtidig -> hak hvert 10. s)
store.entries[key] = {value = value == nil and NOTHING or value, at = time + math.random() * ttlMs * 0.5}
return value
end
function ADCache.clear(bucket)
if bucket == nil then ADCache.buckets = {} else ADCache.buckets[bucket] = nil end
end
+2 -1
View File
@@ -20,7 +20,8 @@ ADFieldJobs.state = {fields = {}, fleet = {}, assignments = {}, fleetMode = "all
-- efter genindlæsning hentes slå-/rivekurset fra maskinen, som CP selv gemmer kurset på)
ADFieldJobs.courses = {}
ADFieldJobs.finished = {}
ADFieldJobs.timer = 0
-- starter halvt forskudt, så markarbejdet og menuens opdatering (begge hvert sekund) ikke rammer samme frame
ADFieldJobs.timer = 500
ADFieldJobs.now = 0
ADFieldJobs.loaded = false
+2 -1
View File
@@ -166,7 +166,8 @@ end
-- Kaldes fra Wait-logikken hvert tick; redirect(task, id) og release(vehicle) er dens egne.
-- true = traktoren er sendt til en silo (Wait-punktet er frigivet).
function ADLoadSwap.tryFromWait(adEnv, task, wait, dt, redirect, release, isReachable)
wait.swapTimerMs = (wait.swapTimerMs or 0) + (dt or 0)
-- tilfældig start (0-2 s): ventende vogne tjekker ikke alle i samme frame
wait.swapTimerMs = (wait.swapTimerMs or math.random(0, ADLoadSwap.CHECK_INTERVAL_MS)) + (dt or 0)
if wait.swapTimerMs < ADLoadSwap.CHECK_INTERVAL_MS then
return false
end
+4 -1
View File
@@ -14,6 +14,8 @@ ADOutbound = {}
ADOutbound.LOG_PREFIX = "ADSmartPickup: "
-- Holdende traktorer spørger planlæggeren så tit (opslag over alle markører).
ADOutbound.CHECK_INTERVAL_MS = 5000
-- spred over HELE intervallet, så genplanlægningerne (og cache-genberegninger) fordeles jævnt
ADOutbound.CHECK_JITTER_MS = 5000
-- "Kør uanset mængde": stiger vognens fyldning ikke så længe ved kilden, køres der med det der er.
ADOutbound.NO_PROGRESS_MS = 15000
-- Objektlager der ikke leverer en palle inden så længe regnes som fastlåst (kendt spilfejl).
@@ -498,7 +500,8 @@ local function startHold(adEnv, vehicle, run, reasons, sourcesById)
warnOnce(adEnv, vehicle, "noWait", string.format("'%s' har intet ledigt Wait-punkt med vejnet (run '%s'); holder ved kilden %s", tostring(ADRuns.vehicleLabel(vehicle)), tostring(run.name), tostring(marker.name)))
end
local previous = holds[vehicle]
holds[vehicle] = {markerId = marker.id, markerName = tostring(marker.name), reasons = reasons, timerMs = 0}
-- tilfældig start (0-2 s) så holdende vogne ikke genplanlægger i samme frame (hak hvert 5. s, 26/9)
holds[vehicle] = {markerId = marker.id, markerName = tostring(marker.name), reasons = reasons, timerMs = math.random(0, ADOutbound.CHECK_JITTER_MS)}
local message = string.format("'%s' holder ved %s: %s", tostring(ADRuns.vehicleLabel(vehicle)), tostring(marker.name), table.concat(reasons, "; "))
if previous == nil or previous.lastLogged ~= message then
Logging.info(ADOutbound.LOG_PREFIX .. "%s", message)
+19 -4
View File
@@ -10,6 +10,13 @@
ADSmartPickup = {}
-- ADCache-opslag der også virker uden adCache (tests)
local function cacheGet(bucket, key, ttlMs, fn)
if ADCacheGet == nil then return fn() end
return ADCacheGet(bucket, key, ttlMs, fn)
end
ADSmartPickup.dir = g_currentModDirectory
if ADSmartPickup.dir ~= nil and source ~= nil then
source(ADSmartPickup.dir .. "gui/SmartPickupFrame.lua")
@@ -216,14 +223,21 @@ local function getUnloadStationAtMarker(adEnv, wayPoint, farmId)
end
-- Station ved et AutoDrive-waypoint (bruges af runs-controlleren og læs-byttet); nil uden station.
-- Markør -> station er stabil (stationer og markører flytter sig sjældent): caches 10 s (ADCache).
ADSmartPickup.STATION_CACHE_MS = 30000
function ADSmartPickup.getLoadStationAtWayPointId(adEnv, wayPointId, farmId)
local wayPoint = adEnv.ADGraphManager:getWayPointById(wayPointId)
return wayPoint ~= nil and getLoadStationAtMarker(adEnv, wayPoint, farmId) or nil
return cacheGet("loadStation", tostring(wayPointId) .. ":" .. tostring(farmId), ADSmartPickup.STATION_CACHE_MS, function()
local wayPoint = adEnv.ADGraphManager:getWayPointById(wayPointId)
return wayPoint ~= nil and getLoadStationAtMarker(adEnv, wayPoint, farmId) or nil
end)
end
function ADSmartPickup.getUnloadStationAtWayPointId(adEnv, wayPointId, farmId)
local wayPoint = adEnv.ADGraphManager:getWayPointById(wayPointId)
return wayPoint ~= nil and getUnloadStationAtMarker(adEnv, wayPoint, farmId) or nil
return cacheGet("unloadStation", tostring(wayPointId) .. ":" .. tostring(farmId), ADSmartPickup.STATION_CACHE_MS, function()
local wayPoint = adEnv.ADGraphManager:getWayPointById(wayPointId)
return wayPoint ~= nil and getUnloadStationAtMarker(adEnv, wayPoint, farmId) or nil
end)
end
-- Løst opslag: nærmeste læssestation inden for maxDistance, uanset køreretning og "bag næsen".
@@ -1194,6 +1208,7 @@ function ADSmartPickup:update(dt)
installHook(adEnv)
isHooked = true
if ADProfiler ~= nil then pcall(ADProfiler.install) end
if ADCache ~= nil then ADCache.enabled = true end
if ADRunsController ~= nil then
pcall(ADRunsController.load, adEnv)
pcall(ADRunsController.installSaveHook)
+21
View File
@@ -5,6 +5,13 @@
ADSources = {}
-- ADCache-opslag der også virker uden adCache (tests)
local function cacheGet(bucket, key, ttlMs, fn)
if ADCacheGet == nil then return fn() end
return ADCacheGet(bucket, key, ttlMs, fn)
end
ADSources.LOG_PREFIX = "ADSmartPickup: "
-- Objektlager tættere end dette på markøren regnes som markørens (samme som triggere).
ADSources.MAX_DISTANCE = 40
@@ -136,6 +143,12 @@ end
-- {kind = "factory"|"silo"|"objectStorage", placeable, station|nil, wayPointId, name} eller nil.
function ADSources.identify(adEnv, wayPointId, farmId)
return cacheGet("identify", tostring(wayPointId) .. ":" .. tostring(farmId), 10000, function()
return ADSources.identifyUncached(adEnv, wayPointId, farmId)
end)
end
function ADSources.identifyUncached(adEnv, wayPointId, farmId)
local wayPoint = adEnv.ADGraphManager:getWayPointById(wayPointId)
if wayPoint == nil then
return nil
@@ -552,6 +565,10 @@ end
-- Markører hvis aflæsningsstation er et rigtigt salgssted: { {marker, station} }.
function ADSources.getSellMarkers(adEnv, farmId)
return cacheGet("sellMarkers", tostring(farmId), 10000, function() return ADSources.findSellMarkers(adEnv, farmId) end)
end
function ADSources.findSellMarkers(adEnv, farmId)
local result = {}
for _, marker in pairs(adEnv.ADGraphManager:getMapMarkers()) do
local station = ADSmartPickup.getUnloadStationAtWayPointId(adEnv, marker.id, farmId)
@@ -999,6 +1016,10 @@ end
-- { {name, kind = "factory"|"silo"|"objectStorage"|"husbandry", entries = {{fillType, name, title, level, capacity, isOutput, count}}} }
-- kun gårdens bygninger med indhold; sorteret på slags, dernæst navn.
function ADSources.getFarmInventory(adEnv, farmId)
return cacheGet("inventory", tostring(farmId), 2000, function() return ADSources.buildFarmInventory(adEnv, farmId) end)
end
function ADSources.buildFarmInventory(adEnv, farmId)
local placeables = g_currentMission ~= nil and g_currentMission.placeableSystem ~= nil and g_currentMission.placeableSystem.placeables or {}
local result = {}
for _, placeable in pairs(placeables) do
+1 -1
View File
@@ -309,7 +309,7 @@ end
-- stående for evigt. Her genoptages ventningen for aktive traktorer i et forsynings-run hvis
-- aflæsningsopgave peger på en Wait-markør: hjem = run'ets mål, og reservationen sættes igen.
-- Kaldes fra ADSmartPickup:update (throttlet).
ADUnloadWait.ADOPT_INTERVAL_MS = 5000
ADUnloadWait.ADOPT_INTERVAL_MS = 5300
local adoptTimerMs = ADUnloadWait.ADOPT_INTERVAL_MS
function ADUnloadWait.adoptOrphans(adEnv)
+28 -6
View File
@@ -5,6 +5,13 @@
ADWaitPool = {}
-- ADCache-opslag der også virker uden adCache (tests)
local function cacheGet(bucket, key, ttlMs, fn)
if ADCacheGet == nil then return fn() end
return ADCacheGet(bucket, key, ttlMs, fn)
end
ADWaitPool.LOG_PREFIX = "ADSmartPickup: "
-- sættes af den der installerer opgave-hooks (task.STATE_DRIVING); 2 er AD's værdi
ADWaitPool.STATE_DRIVING = 2
@@ -14,6 +21,10 @@ ADWaitPool.claims = setmetatable({}, {__mode = "v"})
-- poolGroup: run'ets Wait-mappe; tom/nil = alle Wait-mapper.
function ADWaitPool.getCandidates(adEnv, poolGroup)
return cacheGet("waitCandidates", tostring(poolGroup or ""), 10000, function() return ADWaitPool.findCandidates(adEnv, poolGroup) end)
end
function ADWaitPool.findCandidates(adEnv, poolGroup)
local candidates = {}
for _, marker in pairs(adEnv.ADGraphManager:getMapMarkers()) do
local isInPool = poolGroup == nil or poolGroup == "" or marker.group == poolGroup
@@ -57,20 +68,31 @@ ADWaitPool.OCCUPIED_RADIUS = 6
-- Positioner for ANDRE motoriserede køretøjer (ikke vognen selv eller dens redskaber) — til at se om et
-- Wait-punkt fysisk er optaget, også uden reservation (fx lige efter en omplanlægning).
-- Alle motoriserede køretøjers positioner, delt mellem opslag i 0,5 s (ikke én runde pr. vogn pr. opslag).
local function allVehiclePositions()
return cacheGet("vehiclePositions", "all", 500, function()
local list = {}
local system = g_currentMission ~= nil and g_currentMission.vehicleSystem or nil
for _, other in pairs(system ~= nil and system.vehicles or {}) do
if other.spec_motorized ~= nil and other.rootNode ~= nil then
local ok, x, _, z = pcall(getWorldTranslation, other.rootNode)
if ok and x ~= nil then table.insert(list, {vehicle = other, x = x, z = z}) end
end
end
return list
end)
end
local function otherVehiclePositions(vehicle)
local positions = {}
local system = g_currentMission ~= nil and g_currentMission.vehicleSystem or nil
local own = {}
if vehicle.getChildVehicles ~= nil then
local ok, children = pcall(vehicle.getChildVehicles, vehicle)
for _, child in pairs(ok and children or {}) do own[child] = true end
end
own[vehicle] = true
for _, other in pairs(system ~= nil and system.vehicles or {}) do
if not own[other] and other.spec_motorized ~= nil and other.rootNode ~= nil then
local ok, x, _, z = pcall(getWorldTranslation, other.rootNode)
if ok and x ~= nil then table.insert(positions, {x = x, z = z}) end
end
for _, entry in ipairs(allVehiclePositions()) do
if not own[entry.vehicle] then table.insert(positions, entry) end
end
return positions
end
+2 -1
View File
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<modDesc descVersion="107">
<author>masterdraco</author>
<version>1.27.3.0</version>
<version>1.28.0.0</version>
<title>
<en>AD Smart Pickup</en>
</title>
@@ -12,6 +12,7 @@
<multiplayer supported="true"/>
<l10n filenamePrefix="l10n/l10n"/>
<extraSourceFiles>
<sourceFile filename="adCache.lua"/>
<sourceFile filename="adSupplyPlanner.lua"/>
<sourceFile filename="adRuns.lua"/>
<sourceFile filename="adRunsStorage.lua"/>