perf: add learner performance controls

This commit is contained in:
Xurkon
2026-06-04 16:37:03 -05:00
parent 305e918525
commit f160570a95
4 changed files with 270 additions and 9 deletions
+36 -1
View File
@@ -141,6 +141,26 @@ local function IsDuplicateMessage(serializedData)
return false
end
local function GetLearnerSettings()
if Questie and Questie.dbLearner and Questie.dbLearner.global and Questie.dbLearner.global.settings then
return Questie.dbLearner.global.settings
end
return {}
end
local function GetCommsTuning()
local intensity = GetLearnerSettings().learnerCommsIntensity or "normal"
if intensity == "off" then
return false, 0, 999999, 0, 0
elseif intensity == "low" then
return true, 4, 6.0, 2, 1
elseif intensity == "fast" then
return true, 15, 1.5, 10, 4
end
return true, 9, 3.5, 6, 2
end
function QuestieLearnerComms:Initialize()
DebugLog("DEVELOP", "Initializing QuestieLearnerComms")
@@ -195,6 +215,8 @@ function _QuestieLearnerComms:ProcessReinforcement()
end
function QuestieLearnerComms:BroadcastLearnedData(op, entityType, entityId, data)
local commsEnabled = GetCommsTuning()
if not commsEnabled then return end
if not data or type(data) ~= "table" then return end
-- 1. Create Payload (sanitize data to remove functions before serialization)
@@ -226,11 +248,20 @@ function QuestieLearnerComms:BroadcastLearnedData(op, entityType, entityId, data
end
function _QuestieLearnerComms:QueueMessage(encodedMessage)
local commsEnabled = GetCommsTuning()
if not commsEnabled then return end
rateLimitQueueTail = rateLimitQueueTail + 1
rateLimitQueue[rateLimitQueueTail] = encodedMessage
end
function _QuestieLearnerComms:ProcessQueues()
local commsEnabled, tunedBucketCapacity, tunedMinChatInterval, normalIncomingCount, combatIncomingCount = GetCommsTuning()
bucketCapacity = tunedBucketCapacity
tokenRefillRate = bucketCapacity / bucketWindow
minChatInterval = tunedMinChatInterval
currentTokens = math_min(bucketCapacity, currentTokens)
if not commsEnabled then return end
-- 1. Refill Tokens
local now = GetTime()
local elapsed = now - lastTokenUpdate
@@ -260,7 +291,7 @@ function _QuestieLearnerComms:ProcessQueues()
end
-- 3. Process Incoming Queue (Combat Aware)
local processCount = InCombatLockdown() and 2 or 6
local processCount = InCombatLockdown() and combatIncomingCount or normalIncomingCount
for i = 1, processCount do
if incomingMessageQueueHead > incomingMessageQueueTail then break end
local rawMsg = incomingMessageQueue[incomingMessageQueueHead]
@@ -278,6 +309,8 @@ end
local frame = CreateFrame("Frame")
frame:RegisterEvent("CHAT_MSG_CHANNEL")
frame:SetScript("OnEvent", function(self, event, msg, sender, _, _, _, _, _, channelId, channelName)
local commsEnabled = GetCommsTuning()
if not commsEnabled then return end
if channelName == hiddenChannelName and sender ~= UnitName("player") then
incomingMessageQueueTail = incomingMessageQueueTail + 1
incomingMessageQueue[incomingMessageQueueTail] = {text = msg, sender = sender}
@@ -285,6 +318,8 @@ frame:SetScript("OnEvent", function(self, event, msg, sender, _, _, _, _, _, cha
end)
function QuestieLearnerComms:OnCommReceived(prefix, message, distribution, sender)
local commsEnabled = GetCommsTuning()
if not commsEnabled then return end
if prefix == addonPrefix and sender ~= UnitName("player") then
incomingMessageQueueTail = incomingMessageQueueTail + 1
incomingMessageQueue[incomingMessageQueueTail] = {text = message, sender = sender}
@@ -24,6 +24,57 @@ QuestieOptions.tabs.advanced = {}
local optionsDefaults = QuestieOptionsDefaults:Load()
local _GetLanguages
local function GetLearnerSettings()
Questie.dbLearner = Questie.dbLearner or {}
Questie.dbLearner.global = Questie.dbLearner.global or {}
Questie.dbLearner.global.settings = Questie.dbLearner.global.settings or {}
local settings = Questie.dbLearner.global.settings
if settings.performanceMode == nil then
settings.performanceMode = "balanced"
end
if settings.pinRefreshDelay == nil then
settings.pinRefreshDelay = 0.5
end
if settings.pinRefreshMode == nil then
settings.pinRefreshMode = "batched"
end
if settings.liveNpcUpdateDelay == nil then
settings.liveNpcUpdateDelay = 0.5
end
if settings.learnerCommsIntensity == nil then
settings.learnerCommsIntensity = "normal"
end
if settings.minConfidencePins == nil then
settings.minConfidencePins = 1
end
return settings
end
local function ApplyLearnerPerformancePreset(mode)
local settings = GetLearnerSettings()
settings.performanceMode = mode
if mode == "realtime" then
settings.pinRefreshDelay = 0.1
settings.pinRefreshMode = "immediate"
settings.liveNpcUpdateDelay = 0.25
settings.learnerCommsIntensity = "fast"
settings.minConfidencePins = 1
elseif mode == "low" then
settings.pinRefreshDelay = 2.0
settings.pinRefreshMode = "batched"
settings.liveNpcUpdateDelay = 2.0
settings.learnerCommsIntensity = "low"
settings.minConfidencePins = 3
elseif mode == "balanced" then
settings.pinRefreshDelay = 0.5
settings.pinRefreshMode = "batched"
settings.liveNpcUpdateDelay = 0.5
settings.learnerCommsIntensity = "normal"
settings.minConfidencePins = 1
end
end
function QuestieOptions.tabs.advanced:Initialize()
-- This needs to be called inside of the Init process for l10n to be fully loaded
StaticPopupDialogs["QUESTIE_LANG_CHANGED_RELOAD"] = {
@@ -157,6 +208,120 @@ function QuestieOptions.tabs.advanced:Initialize()
},
},
learnerPerformanceSpacer = QuestieOptionsUtils:Spacer(1.9),
learnerPerformanceHeader = {
type = "header",
order = 2,
name = function() return l10n('QuestieLearner Performance'); end,
},
learnerPerformanceMode = {
type = "select",
order = 2.1,
values = {
realtime = l10n("Realtime"),
balanced = l10n("Balanced"),
low = l10n("Low Impact"),
manual = l10n("Manual"),
},
style = "dropdown",
name = function() return l10n('Performance Mode'); end,
desc = function() return l10n('Controls how aggressively QuestieLearner updates learned pins, live data, and learner comms. Low Impact is recommended for heavy activity zones or low-end computers.'); end,
get = function() return GetLearnerSettings().performanceMode or "balanced" end,
set = function(_, value)
ApplyLearnerPerformancePreset(value)
end,
},
learnerPinRefreshMode = {
type = "select",
order = 2.2,
values = {
immediate = l10n("Immediate"),
batched = l10n("Batched"),
manual = l10n("Manual / Reload"),
},
style = "dropdown",
name = function() return l10n('Pin Refresh Behavior'); end,
desc = function() return l10n('Controls when learned pins refresh after QuestieLearner records new data. Manual / Reload records data but avoids live pin redraws until reload or another Questie refresh.'); end,
get = function() return GetLearnerSettings().pinRefreshMode or "batched" end,
set = function(_, value)
local settings = GetLearnerSettings()
settings.pinRefreshMode = value
settings.performanceMode = "manual"
end,
},
learnerPinRefreshDelay = {
type = "range",
order = 2.3,
name = function() return l10n('Pin Refresh Delay'); end,
desc = function() return l10n('Seconds to wait before refreshing learned quest pins after learner activity. Higher values reduce stutter during kill or loot bursts.'); end,
min = 0.1,
max = 5,
step = 0.1,
width = 1.5,
get = function() return GetLearnerSettings().pinRefreshDelay or 0.5 end,
set = function(_, value)
local settings = GetLearnerSettings()
settings.pinRefreshDelay = value
settings.performanceMode = "manual"
end,
},
learnerLiveNpcUpdateDelay = {
type = "range",
order = 2.4,
name = function() return l10n('Live NPC Update Delay'); end,
desc = function() return l10n('Seconds to batch learned NPC live database updates. Higher values reduce work during combat and crowded zones.'); end,
min = 0.25,
max = 5,
step = 0.25,
width = 1.5,
get = function() return GetLearnerSettings().liveNpcUpdateDelay or 0.5 end,
set = function(_, value)
local settings = GetLearnerSettings()
settings.liveNpcUpdateDelay = value
settings.performanceMode = "manual"
end,
},
learnerMinConfidencePins = {
type = "range",
order = 2.5,
name = function() return l10n('Minimum Kills Before Learned Pins'); end,
desc = function() return l10n('How many matching NPC sightings are needed before QuestieLearner shows learned pins. Higher values reduce one-off pin churn.'); end,
min = 1,
max = 10,
step = 1,
width = 1.5,
get = function() return GetLearnerSettings().minConfidencePins or 1 end,
set = function(_, value)
local settings = GetLearnerSettings()
settings.minConfidencePins = value
settings.performanceMode = "manual"
end,
},
learnerCommsIntensity = {
type = "select",
order = 2.6,
values = {
off = l10n("Off"),
low = l10n("Low"),
normal = l10n("Normal"),
fast = l10n("Fast"),
},
style = "dropdown",
name = function() return l10n('Learner Comms Intensity'); end,
desc = function() return l10n('Controls how much learner data Questie processes and sends through learner comms. Lower values reduce CPU and chat-channel work.'); end,
get = function() return GetLearnerSettings().learnerCommsIntensity or "normal" end,
set = function(_, value)
local settings = GetLearnerSettings()
settings.learnerCommsIntensity = value
settings.performanceMode = "manual"
if value == "off" then
Questie.db.profile.learnerBroadcast = false
elseif Questie.db.profile.learnerBroadcast == false then
Questie.db.profile.learnerBroadcast = true
end
end,
},
Spacer_A = QuestieOptionsUtils:Spacer(2.9),
locale_header = {
type = "header",
@@ -77,6 +77,7 @@ function QuestieOptionsDefaults:Load()
enableTooltipsQuestLevel = true,
showQuestXpAtMaxLevel = true,
enableTooltipsNextInChain = true,
learnerBroadcast = true,
enableMapIcons = true,
enableMiniMapIcons = true,
questieShutUp = false,
+68 -8
View File
@@ -321,6 +321,11 @@ local function EnsureLearnedData()
prioritizeMyData = true,
staleThreshold = 90, -- days
pruneVerified = false, -- protect verified data by default
performanceMode = "balanced",
pinRefreshDelay = 0.5,
pinRefreshMode = "batched",
liveNpcUpdateDelay = 0.5,
learnerCommsIntensity = "normal",
}
else
-- Backfill sub-tables that may be missing from older SavedVariables
@@ -337,6 +342,27 @@ local function EnsureLearnedData()
if s.learnObjects == nil then s.learnObjects = true end
if s.minConfidencePins == nil then s.minConfidencePins = 1 end
if s.prioritizeMyData == nil then s.prioritizeMyData = true end
if s.staleThreshold == nil then
s.staleThreshold = 90
end
if s.pruneVerified == nil then
s.pruneVerified = false
end
if s.performanceMode == nil then
s.performanceMode = "balanced"
end
if s.pinRefreshDelay == nil then
s.pinRefreshDelay = 0.5
end
if s.pinRefreshMode == nil then
s.pinRefreshMode = "batched"
end
if s.liveNpcUpdateDelay == nil then
s.liveNpcUpdateDelay = 0.5
end
if s.learnerCommsIntensity == nil then
s.learnerCommsIntensity = "normal"
end
end
return true
end
@@ -355,6 +381,16 @@ function QuestieLearner:GetSettings()
return Questie.dbLearner.global.settings
end
local function GetLearnerSetting(key, defaultValue)
if Questie and Questie.dbLearner and Questie.dbLearner.global and Questie.dbLearner.global.settings then
local value = Questie.dbLearner.global.settings[key]
if value ~= nil then
return value
end
end
return defaultValue
end
------------------------------------------------------------------------
-- Cross-link engine
-- After ANY entity is learned, scan all other learned data and stitch
@@ -427,6 +463,7 @@ local function _GetDB() return Questie.dbLearner.global end
-- Called after cross-linking so map pins refresh immediately.
local _pendingQuestPinRefreshes = {}
local _pendingQuestPinRefreshTimer = nil
local _pendingQuestFrameUnloads = {}
local function _FlushActiveQuestPins()
local questIdSet = _pendingQuestPinRefreshes
@@ -439,14 +476,24 @@ local function _FlushActiveQuestPins()
for questId in pairs(questIdSet) do
if QuestiePlayer.currentQuestlog[questId] then
if QuestieMap and QuestieMap.UnloadQuestFrames and _pendingQuestFrameUnloads[questId] then
QuestieMap:UnloadQuestFrames(questId)
end
QuestieQuest:UpdateQuest(questId)
end
end
_pendingQuestFrameUnloads = {}
end
local function _RefreshActiveQuestPins(questIdSet)
-- Skip scheduling when the set is empty (avoids no-op timer callbacks)
if not next(questIdSet) then return end
if GetLearnerSetting("pinRefreshMode", "batched") == "manual" then
for questId in pairs(questIdSet) do
_pendingQuestFrameUnloads[questId] = nil
end
return
end
for questId in pairs(questIdSet) do
_pendingQuestPinRefreshes[questId] = true
end
@@ -458,7 +505,7 @@ local function _RefreshActiveQuestPins(questIdSet)
local timer = (C_Timer) or (QuestieCompat and QuestieCompat.C_Timer)
if timer and timer.After then
_pendingQuestPinRefreshTimer = true
timer.After(0.15, _FlushActiveQuestPins)
timer.After(GetLearnerSetting("pinRefreshDelay", 0.5), _FlushActiveQuestPins)
else
_FlushActiveQuestPins()
end
@@ -577,8 +624,10 @@ local function _InvalidateSpawnListsForNPC(npcId)
and (now - _invalidateDebounce[debounceKey]) < debounceWindow
if not suppressUnload then
_invalidateDebounce[debounceKey] = now
if QuestieMap and QuestieMap.UnloadQuestFrames then
if GetLearnerSetting("pinRefreshMode", "batched") == "immediate" and QuestieMap and QuestieMap.UnloadQuestFrames then
QuestieMap:UnloadQuestFrames(questId)
else
_pendingQuestFrameUnloads[questId] = true
end
end
questsToRefresh[questId] = true -- always refresh when data changed
@@ -607,7 +656,6 @@ end
-- arrive within a few frames. Saved learner evidence is updated immediately,
-- but live QuestieDB override/cache invalidation is batched so the large DB
-- layer is not churned on every single kill.
local LIVE_NPC_UPDATE_DELAY = 0.5
local function _ApplyNpcLiveUpdate(npcId)
local existing = Questie.dbLearner
@@ -676,7 +724,7 @@ local function _QueueNpcLiveUpdate(npcId)
local timer = QuestieCompat and QuestieCompat.C_Timer
if timer and timer.After then
_Learner.pendingNpcLiveUpdateTimer = true
timer.After(LIVE_NPC_UPDATE_DELAY, _FlushNpcLiveUpdates)
timer.After(GetLearnerSetting("liveNpcUpdateDelay", 0.5), _FlushNpcLiveUpdates)
else
_FlushNpcLiveUpdates()
end
@@ -3684,6 +3732,13 @@ local function _ValidateLearnedSpawnData(data)
end
function _Learner:BroadcastIfCommsAvailable(typ, id, data)
if Questie and Questie.db and Questie.db.profile and Questie.db.profile.learnerBroadcast == false then
return
end
if GetLearnerSetting("learnerCommsIntensity", "normal") == "off" then
return
end
local QuestieLearnerComms = QuestieLoader:ImportModule("QuestieLearnerComms")
if not (QuestieLearnerComms and QuestieLearnerComms.BroadcastLearnedData) then
return
@@ -3720,14 +3775,19 @@ function _Learner:BroadcastIfCommsAvailable(typ, id, data)
if timer and timer.After then
_Learner.pendingBroadcastTimer = true
timer.After(2, FlushBroadcasts)
local delay = 2
local intensity = GetLearnerSetting("learnerCommsIntensity", "normal")
if intensity == "low" then
delay = 4
elseif intensity == "fast" then
delay = 1
end
timer.After(delay, FlushBroadcasts)
else
FlushBroadcasts()
end
end
local NETWORK_MERGE_DELAY = 0.5
local function _QueueIncomingNetworkMerge(typ, id, data, op)
_Learner.pendingNetworkMerges = _Learner.pendingNetworkMerges or {}
local key = typ .. ":" .. tostring(id)
@@ -3760,7 +3820,7 @@ local function _QueueIncomingNetworkMerge(typ, id, data, op)
if timer and timer.After then
_Learner.pendingNetworkMergeTimer = true
timer.After(NETWORK_MERGE_DELAY, FlushNetworkMerges)
timer.After(GetLearnerSetting("liveNpcUpdateDelay", 0.5), FlushNetworkMerges)
else
FlushNetworkMerges()
end