perf: reduce CPU hotspots identified in profiler

- HBD.lua: Cache GetPlayerWorldPosition/GetPlayerZonePosition at 50ms
  intervals instead of hammering GetPlayerMapPosition() every frame.
  Invalidate cache on PLAYER_ENTERING_WORLD and ZONE_CHANGED_* events.
  Expected ~97% reduction in C API position calls (45,570 -> ~1,200 / 10min).

- zoneDB.lua: Replace O(n) linear scan in GetAreaIdByUiMapId with an
  O(1) reverse lookup cache (uiMapIdToAreaIdCache) built at Initialize().
  Cache is kept in sync by ApplyCustomZones and name-match fallback now
  caches its result so subsequent calls are also O(1).

- Tooltip.lua: Throttle GameTooltip OnUpdate hook to 100ms intervals
  (was firing every frame at 60-144 Hz). Added _tooltipLastText cache
  to avoid redundant GetText() + CountTooltip() calls when nothing changed.

- QuestieArrow.lua: Hoist _HasMissingCompletedFlag, _GetCompleteIconType,
  _CollectFinisherSpawns, and _CollectObjective out of _CollectQuestTargets
  to module-level functions. These were re-created as closures on every
  UpdateNearestTargets call (1 Hz). Shared per-cycle context is published
  via _arrow_* module upvalues to avoid closure capture overhead.

- QuestieLearnerComms.lua: Four micro-optimizations:
  (1) Reduce ProcessQueues ticker 0.2s -> 0.5s (still 7x faster than
      minChatInterval of 3.5s).
  (2) Cache hidden channel ID at init; lazy refresh on disconnect.
  (3) Drop LibDeflate compress level 9 -> 1 (fraction of CPU cost).
  (4) O(1) messageCacheCount counter replaces O(n) pairs() size scan.
This commit is contained in:
Xurkon
2026-03-29 16:42:43 -05:00
parent 2d9d83f265
commit 05ea31bc53
5 changed files with 288 additions and 142 deletions
+35 -26
View File
@@ -30,9 +30,12 @@ ZoneDB.private.dungeons = ZoneDB.private.dungeons or {}
ZoneDB.private.dungeonLocations = ZoneDB.private.dungeonLocations or {}
ZoneDB.private.dungeonParentZones = ZoneDB.private.dungeonParentZones or {}
ZoneDB.private.subZoneToParentZone = ZoneDB.private.subZoneToParentZone or {}
-- O(1) reverse lookup: uiMapId -> areaId, built from uiMapIdToAreaId at init.
ZoneDB.private.uiMapIdToAreaIdCache = ZoneDB.private.uiMapIdToAreaIdCache or {}
local areaIdToUiMapId = ZoneDB.private.areaIdToUiMapId
local uiMapIdToAreaId = ZoneDB.private.uiMapIdToAreaId
local uiMapIdToAreaIdCache = ZoneDB.private.uiMapIdToAreaIdCache
local dungeons = ZoneDB.private.dungeons
local dungeonLocations = ZoneDB.private.dungeonLocations
local dungeonParentZones = ZoneDB.private.dungeonParentZones
@@ -59,6 +62,7 @@ function ZoneDB:Initialize()
end
ZoneDB:ApplyCustomZones()
_ZoneDB:BuildUiMapIdToAreaIdCache()
_ZoneDB:GenerateParentZoneToStartingZoneTable()
-- Run tests if debug enabled
@@ -75,6 +79,10 @@ function ZoneDB:ApplyCustomZones()
-- skip non-numeric keys
elseif _ZoneDB.areaIdToUiMapId[uiMapId] == nil then
_ZoneDB.areaIdToUiMapId[uiMapId] = uiMapId
-- Keep reverse cache in sync
if uiMapIdToAreaIdCache[uiMapId] == nil then
uiMapIdToAreaIdCache[uiMapId] = uiMapId
end
end
if data and type(data.parentMapID) == "number" and _ZoneDB.areaIdToUiMapId[data.parentMapID] == nil then
@@ -83,6 +91,17 @@ function ZoneDB:ApplyCustomZones()
end
end
-- Builds the O(1) uiMapId -> areaId reverse cache from the uiMapIdToAreaId table.
-- Called once at Initialize() after all zone data is loaded.
function _ZoneDB:BuildUiMapIdToAreaIdCache()
for areaUiMapId, areaId in next, uiMapIdToAreaId do
-- First entry wins (matches the original scan behaviour)
if uiMapIdToAreaIdCache[areaUiMapId] == nil then
uiMapIdToAreaIdCache[areaUiMapId] = areaId
end
end
end
function _ZoneDB:GenerateParentZoneToStartingZoneTable()
for startingZone, parentZone in next, subZoneToParentZone do
parentZoneToSubZone[parentZone] = startingZone
@@ -110,46 +129,36 @@ function ZoneDB:GetUiMapIdByAreaId(areaId)
return nil
end
--- Use with care, kind of slow.
---@param uiMapId UiMapId
---@return AreaId
function ZoneDB:GetAreaIdByUiMapId(uiMapId)
--? Some areas have multiple areaIds, so we return the correct AreaId
-- Fast path: override table
if UiMapIdOverrides[uiMapId] then
return UiMapIdOverrides[uiMapId]
end
local foundId
-- First we look for a direct match
for AreaUiMapId, lAreaId in next, uiMapIdToAreaId do
local areaId = lAreaId
if (AreaUiMapId == uiMapId and not foundId) then
foundId = areaId
elseif AreaUiMapId == uiMapId and foundId ~= AreaUiMapId then
-- If we find a second match that does not match the first
-- Only print if debug is enabled.
if Questie.db.profile.debugEnabled then
Questie:Error("[ZoneDB:GetAreaIdByUiMapId] : ", "UiMapId", uiMapId, "has multiple AreaIds:", foundId, areaId)
end
end
end
if foundId then
return foundId
else
-- As a last resort we try to match AreaId and UiMapId by name
-- Fast path: O(1) pre-built reverse cache
local cached = uiMapIdToAreaIdCache[uiMapId]
if cached then return cached end
-- Slow fallback: name-based lookup (only for unmapped IDs, result is cached for next time)
local mapInfo = C_Map.GetMapInfo(uiMapId)
if mapInfo then
for areaId in next, areaIdToUiMapId do
local mapInfo = C_Map.GetMapInfo(uiMapId)
local areaName = C_Map.GetAreaInfo(areaId)
if mapInfo and mapInfo.name == areaName then
if mapInfo.name == areaName then
Questie:Debug(Questie.DEBUG_DEVELOP, "[ZoneDB:GetAreaIdByUiMapId] : ", "Found AreaId", areaName, ":", areaId, "for UiMapId", mapInfo.name, ":", uiMapId, "by name")
-- Cache the result so we don't scan again
uiMapIdToAreaIdCache[uiMapId] = areaId
return areaId
end
end
if Questie.db.profile.debugEnabled then
Questie:Debug(Questie.DEBUG_DEVELOP, "No AreaId found for UiMapId: " .. uiMapId .. ":" .. (C_Map.GetMapInfo(uiMapId) and C_Map.GetMapInfo(uiMapId).name or "nil"))
end
return nil
end
if Questie.db.profile.debugEnabled then
Questie:Debug(Questie.DEBUG_DEVELOP, "No AreaId found for UiMapId: " .. uiMapId .. ":" .. (mapInfo and mapInfo.name or "nil"))
end
return nil
end
---@param areaId AreaId