Files
CawDPSMeter/CawDiagnostics.lua

260 lines
11 KiB
Lua

-- Caw DPS Meter persistent diagnostics bootstrap (RC37)
-- Loaded first so Caw load-time Lua errors can be retained until SavedVariables are ready.
CAW_DPS_METER = CAW_DPS_METER or {}
local D = CAW_DPS_METER
D.diagVersion = "RC37-1"
D.diagMaxEntries = 400
D.diagBuffer = D.diagBuffer or {}
D.diagInHandler = false
D.diagSession = tostring({})
function D.diagStamp()
if date then return date("%Y-%m-%d %H:%M:%S") end
return ""
end
function D.diagNow()
return GetTime and GetTime() or 0
end
function D.diagEnsureDB()
CawDPSMeterErrorLog = CawDPSMeterErrorLog or {}
-- RC34 accidentally treated the diagnostics handler's own stack frame as
-- proof that every Lua error belonged to Caw. Drop that polluted RC34 log
-- once on upgrade so RC35 starts with trustworthy data.
if CawDPSMeterErrorLog.loggerVersion=="RC34-1" then
CawDPSMeterErrorLog.entries={}
CawDPSMeterErrorLog.total=0
CawDPSMeterErrorLog.dropped=0
end
CawDPSMeterErrorLog.schema = 1
CawDPSMeterErrorLog.loggerVersion = D.diagVersion
CawDPSMeterErrorLog.entries = CawDPSMeterErrorLog.entries or {}
CawDPSMeterErrorLog.total = CawDPSMeterErrorLog.total or 0
CawDPSMeterErrorLog.dropped = CawDPSMeterErrorLog.dropped or 0
return CawDPSMeterErrorLog
end
function D.diagInsert(rec)
if not rec then return end
local db = CawDPSMeterErrorLog
if not db or not db.entries then
table.insert(D.diagBuffer, rec)
while table.getn(D.diagBuffer) > 80 do table.remove(D.diagBuffer,1) end
return
end
local entries = db.entries
local n = table.getn(entries)
local last = n > 0 and entries[n] or nil
-- Collapse identical warnings/errors that repeat rapidly (timeouts, ambiguous pets, etc.).
if last and last.session==rec.session and last.level==rec.level and last.code==rec.code and last.message==rec.message
and last.context==rec.context and last.stack==rec.stack
and (rec.gameTime or 0)-(last.gameTime or 0)>=0 and (rec.gameTime or 0)-(last.gameTime or 0)<=10 then
last.count=(last.count or 1)+1
last.lastSeen=rec.stamp
last.gameTime=rec.gameTime
db.total=(db.total or 0)+1
return
end
table.insert(entries,rec)
db.total=(db.total or 0)+1
while table.getn(entries) > (D.diagMaxEntries or 400) do
table.remove(entries,1)
db.dropped=(db.dropped or 0)+1
end
end
function D.diagLog(level,code,message,context,stack)
local rec={
session=D.diagSession,
stamp=D.diagStamp(), gameTime=D.diagNow(),
level=level or "INFO", code=code or "GENERAL",
message=tostring(message or ""), context=context,
stack=stack, addonVersion=D.version or "load-time", count=1
}
D.diagInsert(rec)
end
function D.diagWarn(code,message,context) D.diagLog("WARN",code,message,context,nil) end
function D.diagCal(code,message,context) D.diagLog("CAL",code,message,context,nil) end
function D.diagError(code,message,context,stack) D.diagLog("ERROR",code,message,context,stack) end
-- Bounded summaries for on-demand target sync. Never retain packet payloads,
-- actor tables, player names or combat events, and never send extra traffic.
function D.targetSyncDiag(code,job)
local db=D.diagEnsureDB()
local history=db.targetSync
if type(history)~="table" or history.schema~=1 then
history={schema=1,sessions={}}; db.targetSync=history
end
local s=D.targetSyncDiagSession
if not s then
s={version=D.version,startedAt=D.diagStamp(),counts={},recent={}}
D.targetSyncDiagSession=s; table.insert(history.sessions,s)
while table.getn(history.sessions)>3 do table.remove(history.sessions,1) end
end
s.counts[code]=(s.counts[code] or 0)+1
local r={code=code,stamp=D.diagStamp()}
if job then
r.kind=job.kind; r.segment=job.context and job.context.segment; r.channel=job.channel
local b=job.buffer
if b then
r.expectedActors=b.ac; r.expectedTargets=b.tc; r.expectedRows=b.rc
r.receivedActors=b.actorsSeen; r.receivedTargets=b.targetsSeen; r.receivedRows=b.rowsSeen
end
end
s.last=code; s.updatedAt=r.stamp; table.insert(s.recent,r)
while table.getn(s.recent)>24 do table.remove(s.recent,1) end
s.parserEnabled=D.parserEnabled(); s.syncEnabled=D.combatSyncEnabled()
s.input=D.dpsLogActive and "DPSLog" or (D.dpsLogProbing and "probing" or "combat-text")
end
function D.targetSyncDiagSave()
if D.targetSyncRequest or D.targetSyncPlan or D.targetSyncOut then
D.targetSyncDiag("RELOAD_INTERRUPTED",D.targetSyncRequest or D.targetSyncOut)
end
local s=D.targetSyncDiagSession
if s then s.endedAt=D.diagStamp() end
end
function D.diagFlushBuffer()
D.diagEnsureDB()
local i=1
while i<=table.getn(D.diagBuffer) do D.diagInsert(D.diagBuffer[i]); i=i+1 end
D.diagBuffer={}
end
-- Preserve Blizzard's/current client's error presentation while passively recording Caw errors.
if geterrorhandler and seterrorhandler and not D.diagErrorHandlerInstalled then
D.diagPreviousErrorHandler=geterrorhandler()
D.diagErrorHandlerInstalled=true
seterrorhandler(function(err)
if D.diagInHandler then return end
D.diagInHandler=true
local msg=tostring(err or "unknown Lua error")
local stack=nil
if debugstack then
local ok,s=pcall(debugstack,2,12,12)
if ok then stack=s end
end
-- The handler itself necessarily appears in debugstack(). RC34 searched
-- the whole stack for "CawDPSMeter", so CawDiagnostics.lua caused errors
-- from unrelated addons to be recorded as ours. Ignore our handler line
-- and inspect the actual error message / remaining stack only.
local cleanStack=tostring(stack or "")
cleanStack=string.gsub(cleanStack,"[^\n]*CawDiagnostics%.lua[^\n]*\n?","")
local hay=msg.."\n"..cleanStack
if string.find(hay,"Interface\\AddOns\\CawDPSMeter\\",1,true) or string.find(msg,"CawDPSMeter",1,true) or string.find(msg,"CawThreat",1,true) or string.find(msg,"CAW_DPS_METER",1,true) then
D.diagError("LUA",msg,nil,stack)
end
local prev=D.diagPreviousErrorHandler
if prev then pcall(prev,err) end
D.diagInHandler=false
end)
end
-- RC36 one-time SavedVariables housekeeping.
-- CawDPSMeterLog is still retained as a declared SavedVariable because the
-- old developer commands can intentionally populate it. We only delete the
-- stale pre-RC36 payload once, then mark the migration complete so a freshly
-- captured developer log survives later reloads. Empty calibration sessions
-- are safe to discard; sessions containing any captured data are preserved.
-- Keep only the most recent `max` entries of an array-style table, in order.
local function trimTail(t,max)
if not t or not max then return t end
local n=table.getn(t)
if n<=max then return t end
local kept={}
local i
for i=n-max+1,n do table.insert(kept,t[i]) end
return kept
end
function D.cleanupLegacySavedVariables()
CawDPSMeterDB=CawDPSMeterDB or {}
local version=CawDPSMeterDB.savedVariablesCleanupVersion or 0
if version<1 then
local clearedLegacyLog=false
local removedEmptyCalibration=0
if CawDPSMeterLog~=nil then
CawDPSMeterLog=nil
clearedLegacyLog=true
end
if CawThreatCalibrationDB and CawThreatCalibrationDB.sessions then
local kept={}
local i=1
while i<=table.getn(CawThreatCalibrationDB.sessions) do
local sess=CawThreatCalibrationDB.sessions[i]
local hasData=false
if sess then
if (sess.requests or 0)>0 or (sess.responses or 0)>0 then hasData=true end
if sess.events and table.getn(sess.events)>0 then hasData=true end
if sess.casts and table.getn(sess.casts)>0 then hasData=true end
if sess.snapshots and table.getn(sess.snapshots)>0 then hasData=true end
end
if hasData then
table.insert(kept,sess)
else
removedEmptyCalibration=removedEmptyCalibration+1
end
i=i+1
end
CawThreatCalibrationDB.sessions=kept
end
CawDPSMeterDB.savedVariablesCleanupVersion=1
CawDPSMeterDB.savedVariablesCleanupAt=D.diagStamp()
D.diagLog("INFO","SV_CLEANUP","legacy SavedVariables cleanup",
"legacyLog="..(clearedLegacyLog and "cleared" or "none")..
" emptyCalibrationSessions="..tostring(removedEmptyCalibration),nil)
version=1
end
-- The account-wide file grew to tens of megabytes because the old
-- per-session caps (12 sessions x 12k events/6k casts/6k snapshots/2k actor
-- contexts) were far too generous once serialized as Lua source text. A
-- truncated write of a file that size corrupts every SavedVariables table
-- in the same file, including window layout. Trim any history that is
-- already on disk down to the new, much smaller caps on next login so
-- existing bloated accounts recover without waiting for 12 sessions to
-- cycle out naturally.
if version<2 then
local sessionCap=D.threatCalMaxSessions or 3
if CawThreatCalibrationDB and CawThreatCalibrationDB.sessions then
local sessions=CawThreatCalibrationDB.sessions
while table.getn(sessions)>sessionCap do table.remove(sessions,1) end
local i
for i=1,table.getn(sessions) do
local s=sessions[i]
if s then
s.events=trimTail(s.events,D.threatCalMaxEvents or 2000)
s.casts=trimTail(s.casts,D.threatCalMaxCasts or 1500)
s.snapshots=trimTail(s.snapshots,D.threatCalMaxSnapshots or 800)
s.targets=trimTail(s.targets,D.threatCalMaxTargets or 500)
s.requestContexts=trimTail(s.requestContexts,D.threatCalMaxRequestContexts or 500)
s.actorContexts=trimTail(s.actorContexts,D.threatCalMaxActorContexts or 300)
end
end
end
CawDPSMeterDB.savedVariablesCleanupVersion=2
CawDPSMeterDB.savedVariablesCleanupAt=D.diagStamp()
D.diagLog("INFO","SV_CLEANUP_V2","trimmed oversized calibration history to new caps",nil,nil)
end
end
D.diagBootstrapFrame=D.diagBootstrapFrame or CreateFrame("Frame")
D.diagBootstrapFrame:RegisterEvent("ADDON_LOADED")
D.diagBootstrapFrame:RegisterEvent("PLAYER_LOGOUT")
D.diagBootstrapFrame:SetScript("OnEvent",function()
if event=="ADDON_LOADED" and (arg1=="CawDPSMeter" or arg1==nil) then
D.diagFlushBuffer()
D.cleanupLegacySavedVariables()
elseif event=="PLAYER_LOGOUT" then
D.diagFlushBuffer()
D.targetSyncDiagSave()
end
end)