diff --git a/Core/MultiBot.lua b/Core/MultiBot.lua index 31c71f7..5f03604 100644 --- a/Core/MultiBot.lua +++ b/Core/MultiBot.lua @@ -1285,13 +1285,35 @@ local function MB_tostring(v) return tostring(v) end function MultiBot.dprint(...) - if not MultiBot.debug then return end + local debugApi = MultiBot.Debug + if type(debugApi) == "table" and type(debugApi.IsEnabled) == "function" then + if not debugApi.IsEnabled("core") then + return + end + MultiBot.debug = true + elseif not MultiBot.debug then + return + end + local parts = {} for i=1,select("#", ...) do parts[#parts+1] = MB_tostring(select(i, ...)) end + + local message = "|cffff7f00[MultiBot]|r " .. table.concat(parts, " ") + if type(debugApi) == "table" and type(debugApi.PrintRateLimited) == "function" then + local rateKey = "core.dprint." .. string.lower(tostring(select(1, ...) or "generic")) + debugApi.PrintRateLimited(rateKey, 0.2, "core", message) + return + end + + if type(debugApi) == "table" and type(debugApi.Print) == "function" then + debugApi.Print("core", message) + return + end + if DEFAULT_CHAT_FRAME and DEFAULT_CHAT_FRAME.AddMessage then - DEFAULT_CHAT_FRAME:AddMessage("|cffff7f00[MultiBot]|r ".. table.concat(parts, " ")) + DEFAULT_CHAT_FRAME:AddMessage(message) else print("[MultiBot] ".. table.concat(parts, " ")) end diff --git a/Core/MultiBotAsync.lua b/Core/MultiBotAsync.lua index c17347e..e9bac02 100644 --- a/Core/MultiBotAsync.lua +++ b/Core/MultiBotAsync.lua @@ -1,5 +1,23 @@ if not MultiBot then return end +local function perfCount(counterName, delta) + local debugApi = MultiBot and MultiBot.Debug + if type(debugApi) ~= "table" or type(debugApi.IncrementCounter) ~= "function" or type(debugApi.IsPerfEnabled) ~= "function" or not debugApi.IsPerfEnabled() then + return + end + + debugApi.IncrementCounter(counterName, delta) +end + +local function perfDuration(counterName, elapsed) + local debugApi = MultiBot and MultiBot.Debug + if type(debugApi) ~= "table" or type(debugApi.AddDuration) ~= "function" or type(debugApi.IsPerfEnabled) ~= "function" or not debugApi.IsPerfEnabled() then + return + end + + debugApi.AddDuration(counterName, elapsed) +end + local sharedTimerAfter = MultiBot.TimerAfter or _G.TimerAfter if type(sharedTimerAfter) ~= "function" then @@ -15,22 +33,28 @@ if type(sharedTimerAfter) ~= "function" then end sharedTimerAfter = function(delay, callback) + perfCount("scheduler.timerafter.calls") if type(callback) ~= "function" then return nil end local waitTime = math.max(tonumber(delay) or 0, 0) + perfDuration("scheduler.timerafter.delay_total", waitTime) if type(C_Timer) == "table" and type(C_Timer.After) == "function" then + perfCount("scheduler.timerafter.ctime") return C_Timer.After(waitTime, callback) end -- M11 ownership: keep this fallback OnUpdate local to the scheduler wrapper only. -- Reason: legacy compatibility when C_Timer.After is unavailable. + perfCount("scheduler.timerafter.fallback") local timerFrame = CreateFrame("Frame") local elapsed = 0 timerFrame:SetScript("OnUpdate", function(self, dt) + perfCount("scheduler.fallback.onupdate.calls") + perfDuration("scheduler.fallback.onupdate.elapsed", tonumber(dt) or 0) elapsed = elapsed + (tonumber(dt) or 0) if elapsed < waitTime then return @@ -50,6 +74,7 @@ _G.TimerAfter = sharedTimerAfter -- M11 scheduler contract: -- TimerAfter/NextTick are the only delay APIs that should be used outside this file. function MultiBot.NextTick(callback) + perfCount("scheduler.nexttick.calls") if type(callback) ~= "function" then return nil end diff --git a/Core/MultiBotDebug.lua b/Core/MultiBotDebug.lua index 51c548b..e9cdbe7 100644 --- a/Core/MultiBotDebug.lua +++ b/Core/MultiBotDebug.lua @@ -4,6 +4,98 @@ MultiBot.Debug = MultiBot.Debug or {} +local Debug = MultiBot.Debug + +local DEFAULT_DEBUG_FLAGS = { + core = false, + options = false, + scheduler = false, + roster = false, + quests = false, + spellbook = false, + migration = false, + perf = false, +} + +local function normalizeSubsystem(subsystem) + if type(subsystem) ~= "string" then + return nil + end + + local cleaned = subsystem:lower():gsub("^%s+", ""):gsub("%s+$", "") + if cleaned == "" then + return nil + end + + return cleaned +end + +local function ensureFlagsStore() + if type(MultiBot._debugFlags) ~= "table" then + MultiBot._debugFlags = {} + end + + for subsystem, enabled in pairs(DEFAULT_DEBUG_FLAGS) do + if MultiBot._debugFlags[subsystem] == nil then + MultiBot._debugFlags[subsystem] = enabled and true or false + end + end + + return MultiBot._debugFlags +end + + +local function ensureCountersStore() + if type(MultiBot._debugPerfCounters) ~= "table" then + MultiBot._debugPerfCounters = {} + end + return MultiBot._debugPerfCounters +end + + +local function ensureRateLimitStore() + if type(MultiBot._debugRateLimits) ~= "table" then + MultiBot._debugRateLimits = {} + end + return MultiBot._debugRateLimits +end + +local function shouldEmitRateLimited(key, minInterval) + local rateStore = ensureRateLimitStore() + local now = type(GetTime) == "function" and GetTime() or 0 + local threshold = math.max(tonumber(minInterval) or 0, 0) + local last = tonumber(rateStore[key]) or -math.huge + + if (now - last) < threshold then + return false + end + + rateStore[key] = now + return true +end + +local function normalizeCounterName(counterName) + if type(counterName) ~= "string" then + return nil + end + + local cleaned = counterName:lower():gsub("^%s+", ""):gsub("%s+$", "") + if cleaned == "" then + return nil + end + + return cleaned +end + +local function parsePrintArgs(subsystemOrMessage, messageOrColor, colorHex) + local subsystem = normalizeSubsystem(subsystemOrMessage) + if subsystem then + return subsystem, messageOrColor, colorHex + end + + return nil, subsystemOrMessage, messageOrColor +end + local function EmitDebugMessage(message, colorHex) local text = message if colorHex and colorHex ~= "" then @@ -17,13 +109,103 @@ local function EmitDebugMessage(message, colorHex) end end -function MultiBot.Debug.Print(message, colorHex) - EmitDebugMessage(tostring(message), colorHex) +function Debug.GetFlags() + local flags = ensureFlagsStore() + local snapshot = {} + for subsystem, enabled in pairs(flags) do + snapshot[subsystem] = enabled and true or false + end + return snapshot +end + +function Debug.IsEnabled(subsystem) + local normalized = normalizeSubsystem(subsystem) + if not normalized then + return false + end + + local flags = ensureFlagsStore() + return flags[normalized] == true +end + +function Debug.IsPerfEnabled() + local flags = MultiBot._debugFlags + if type(flags) == "table" and flags.perf ~= nil then + return flags.perf == true + end + + return Debug.IsEnabled("perf") +end + +function Debug.SetEnabled(subsystem, enabled) + local normalized = normalizeSubsystem(subsystem) + if not normalized then + return false + end + + local flags = ensureFlagsStore() + flags[normalized] = enabled and true or false + + if normalized == "core" then + MultiBot.debug = flags[normalized] + end + + return true +end + +function Debug.SetAllEnabled(enabled) + local flags = ensureFlagsStore() + local target = enabled and true or false + for subsystem in pairs(flags) do + flags[subsystem] = target + end + MultiBot.debug = flags.core == true +end + +function Debug.Toggle(subsystem) + local normalized = normalizeSubsystem(subsystem) + if not normalized then + return nil + end + + local flags = ensureFlagsStore() + local nextValue = flags[normalized] ~= true + flags[normalized] = nextValue + + if normalized == "core" then + MultiBot.debug = nextValue + end + + return nextValue +end + +function Debug.Print(subsystemOrMessage, messageOrColor, colorHex) + local subsystem, message, color = parsePrintArgs(subsystemOrMessage, messageOrColor, colorHex) + if subsystem and not Debug.IsEnabled(subsystem) then + return + end + + EmitDebugMessage(tostring(message), color) +end + +function Debug.PrintRateLimited(rateKey, minInterval, subsystemOrMessage, messageOrColor, colorHex) + local key = normalizeCounterName(rateKey) or "debug.print" + if not shouldEmitRateLimited(key, minInterval) then + return false + end + + Debug.Print(subsystemOrMessage, messageOrColor, colorHex) + return true end -function MultiBot.Debug.Once(key, message, colorHex) +function Debug.Once(key, subsystemOrMessage, messageOrColor, colorHex) if type(key) ~= "string" or key == "" then - MultiBot.Debug.Print(message, colorHex) + Debug.Print(subsystemOrMessage, messageOrColor, colorHex) + return + end + + local subsystem, message, color = parsePrintArgs(subsystemOrMessage, messageOrColor, colorHex) + if subsystem and not Debug.IsEnabled(subsystem) then return end @@ -33,18 +215,18 @@ function MultiBot.Debug.Once(key, message, colorHex) end MultiBot._debugOnceFlags[key] = true - MultiBot.Debug.Print(message, colorHex) + EmitDebugMessage(tostring(message), color) end -function MultiBot.Debug.OptionsPath(path, detail) +function Debug.OptionsPath(path, detail) local message = string.format("MultiBot Options: using %s path", tostring(path)) if detail and detail ~= "" then message = message .. string.format(" (%s)", detail) end - MultiBot.Debug.Once("options.path", message, "33ff99") + Debug.Once("options.path", "options", message, "33ff99") end -function MultiBot.Debug.AceGUILoadState(reason) +function Debug.AceGUILoadState(reason) local hasLibStub = type(LibStub) == "table" local aceMinor = nil local aceLoaded = false @@ -62,5 +244,106 @@ function MultiBot.Debug.AceGUILoadState(reason) tostring(aceLoaded) ) - MultiBot.Debug.Once("options.acegui.load", message, "ffff00") -end \ No newline at end of file + Debug.Once("options.acegui.load", "options", message, "ffff00") +end + +function Debug.ListFlagsText() + local flags = Debug.GetFlags() + local keys = {} + for subsystem in pairs(flags) do + keys[#keys + 1] = subsystem + end + table.sort(keys) + + local parts = {} + for _, subsystem in ipairs(keys) do + parts[#parts + 1] = string.format("%s=%s", subsystem, flags[subsystem] and "on" or "off") + end + + return table.concat(parts, ", ") +end + + +function Debug.IncrementCounter(counterName, delta) + if not Debug.IsPerfEnabled() then + return nil + end + + local key = normalizeCounterName(counterName) + if not key then + return nil + end + + local increment = tonumber(delta) or 1 + local counters = ensureCountersStore() + counters[key] = (tonumber(counters[key]) or 0) + increment + return counters[key] +end + +function Debug.AddDuration(counterName, elapsed) + return Debug.IncrementCounter(counterName, tonumber(elapsed) or 0) +end + +function Debug.GetCounters() + local counters = ensureCountersStore() + local snapshot = {} + for key, value in pairs(counters) do + snapshot[key] = value + end + return snapshot +end + +function Debug.ResetCounters(prefix) + local counters = ensureCountersStore() + local normalizedPrefix = normalizeCounterName(prefix) + + if not normalizedPrefix then + for key in pairs(counters) do + counters[key] = nil + end + return + end + + local pattern = "^" .. normalizedPrefix + for key in pairs(counters) do + if string.match(key, pattern) then + counters[key] = nil + end + end +end + +function Debug.FormatCounters(limit) + local counters = Debug.GetCounters() + local keys = {} + for key in pairs(counters) do + keys[#keys + 1] = key + end + table.sort(keys) + + local maxItems = math.floor(tonumber(limit) or 20) + if maxItems < 1 then + maxItems = 1 + end + + local parts = {} + local count = 0 + for _, key in ipairs(keys) do + count = count + 1 + if count > maxItems then + break + end + parts[#parts + 1] = string.format("%s=%.3f", key, tonumber(counters[key]) or 0) + end + + if #parts == 0 then + return "(empty)" + end + + if #keys > maxItems then + parts[#parts + 1] = string.format("... +%d", #keys - maxItems) + end + + return table.concat(parts, ", ") +end + +ensureFlagsStore() \ No newline at end of file diff --git a/Core/MultiBotHandler.lua b/Core/MultiBotHandler.lua index 734ac31..d18f7a5 100644 --- a/Core/MultiBotHandler.lua +++ b/Core/MultiBotHandler.lua @@ -2,7 +2,27 @@ -- M11 ownership: keep this OnUpdate local. -- Reason: automation core hot path (invite/talent/stats/sort) depends on frame-level cadence. +local function perfCount(counterName, delta) + local debugApi = MultiBot and MultiBot.Debug + if type(debugApi) ~= "table" or type(debugApi.IncrementCounter) ~= "function" or type(debugApi.IsPerfEnabled) ~= "function" or not debugApi.IsPerfEnabled() then + return + end + + debugApi.IncrementCounter(counterName, delta) +end + +local function perfDuration(counterName, elapsed) + local debugApi = MultiBot and MultiBot.Debug + if type(debugApi) ~= "table" or type(debugApi.AddDuration) ~= "function" or type(debugApi.IsPerfEnabled) ~= "function" or not debugApi.IsPerfEnabled() then + return + end + + debugApi.AddDuration(counterName, elapsed) +end + function MultiBot.HandleOnUpdate(pElapsed) + perfCount("handler.onupdate.calls") + perfDuration("handler.onupdate.elapsed", tonumber(pElapsed) or 0) if(MultiBot.auto.invite) then MultiBot.timer.invite.elapsed = MultiBot.timer.invite.elapsed + pElapsed end if(MultiBot.auto.talent) then MultiBot.timer.talent.elapsed = MultiBot.timer.talent.elapsed + pElapsed end if(MultiBot.auto.stats) then MultiBot.timer.stats.elapsed = MultiBot.timer.stats.elapsed + pElapsed end @@ -1198,6 +1218,10 @@ end function MultiBot.HandleMultiBotEvent(event, ...) local arg1, arg2 = ... + perfCount("events.total") + if type(event) == "string" then + perfCount("events." .. string.lower(event)) + end if(event == "PLAYER_LOGOUT") then saveBoundFramePoints() savePortalMemory() @@ -1610,6 +1634,7 @@ function MultiBot.HandleMultiBotEvent(event, ...) -- CHAT:WHISPER -- if(event == "CHAT_MSG_WHISPER") then + perfCount("events.chat_msg_whisper") -- Glyphs start local rawMsg, author = arg1, arg2 @@ -2240,6 +2265,107 @@ local function MainBarLayoutResetCommand() printToChat("[MB] Reset layout échoué.") end +local function normalizeDebugToken(value) + if type(value) ~= "string" then + return nil + end + + local cleaned = value:lower():gsub("^%s+", ""):gsub("%s+$", "") + if cleaned == "" then + return nil + end + + return cleaned +end + +local function parseDebugCommandArgs(msg) + local action, subsystem = string.match(tostring(msg or ""), "^%s*(%S*)%s*(.-)%s*$") + return normalizeDebugToken(action), normalizeDebugToken(subsystem) +end + +local function DebugCommand(msg) + local debugApi = MultiBot.Debug + if type(debugApi) ~= "table" then + printToChat("[MB] Debug API indisponible.") + return + end + + local action, subsystem = parseDebugCommandArgs(msg) + if not action or action == "list" then + local text = (type(debugApi.ListFlagsText) == "function") and debugApi.ListFlagsText() or "" + printToChat("[MB] Debug flags: " .. (text ~= "" and text or "(none)")) + return + end + + if action == "all" then + if type(debugApi.SetAllEnabled) ~= "function" then + printToChat("[MB] Action indisponible: all") + return + end + + debugApi.SetAllEnabled(subsystem == "on") + printToChat("[MB] Debug all => " .. ((subsystem == "on") and "on" or "off")) + return + end + + if action == "counters" then + if type(debugApi.FormatCounters) ~= "function" then + printToChat("[MB] Action indisponible: counters") + return + end + + if subsystem == "reset" and type(debugApi.ResetCounters) == "function" then + debugApi.ResetCounters() + printToChat("[MB] Compteurs perf réinitialisés.") + return + end + + printToChat("[MB] Perf counters: " .. debugApi.FormatCounters(25)) + return + end + + if not subsystem then + printToChat("[MB] Usage: /mbdebug list | /mbdebug on | /mbdebug off | /mbdebug toggle | /mbdebug all on|off | /mbdebug counters [reset]") + return + end + + if action == "on" then + if debugApi.SetEnabled and debugApi.SetEnabled(subsystem, true) then + printToChat("[MB] Debug " .. subsystem .. " => on") + else + printToChat("[MB] Sous-système invalide: " .. subsystem) + end + return + end + + if action == "off" then + if debugApi.SetEnabled and debugApi.SetEnabled(subsystem, false) then + printToChat("[MB] Debug " .. subsystem .. " => off") + else + printToChat("[MB] Sous-système invalide: " .. subsystem) + end + return + end + + if action == "toggle" then + if not debugApi.Toggle then + printToChat("[MB] Action indisponible: toggle") + return + end + + local value = debugApi.Toggle(subsystem) + if value == nil then + printToChat("[MB] Sous-système invalide: " .. subsystem) + return + end + + printToChat("[MB] Debug " .. subsystem .. " => " .. (value and "on" or "off")) + return + end + + printToChat("[MB] Action inconnue: " .. action) +end + local COMMAND_DEFINITIONS = { { "MULTIBOT", ToggleMultiBotUI, { "multibot", "mbot", "mb" } }, { "MBFAKEGM", FakeGMCommand, { "mbfakegm" } }, @@ -2252,6 +2378,7 @@ local COMMAND_DEFINITIONS = { { "MBLAYOUTSHOWPAYLOAD", MainBarLayoutShowPayloadCommand, { "mblayoutshowpayload", "mblp" } }, { "MBLAYOUTDELETE", MainBarLayoutDeleteCommand, { "mblayoutdelete", "mbldel" } }, { "MBLAYOUTRESET", MainBarLayoutResetCommand, { "mblayoutreset", "mblreset" } }, + { "MBDEBUG", DebugCommand, { "mbdebug" } }, } for _, def in ipairs(COMMAND_DEFINITIONS) do diff --git a/Core/MultiBotThrottle.lua b/Core/MultiBotThrottle.lua index cd5d535..d2fcd20 100644 --- a/Core/MultiBotThrottle.lua +++ b/Core/MultiBotThrottle.lua @@ -10,6 +10,24 @@ end function MultiBot.Throttle_Init() if MultiBot._throttleInited then return end + local function perfCount(counterName, delta) + local debugApi = MultiBot and MultiBot.Debug + if type(debugApi) ~= "table" or type(debugApi.IncrementCounter) ~= "function" or type(debugApi.IsPerfEnabled) ~= "function" or not debugApi.IsPerfEnabled() then + return + end + + debugApi.IncrementCounter(counterName, delta) + end + + local function perfDuration(counterName, elapsed) + local debugApi = MultiBot and MultiBot.Debug + if type(debugApi) ~= "table" or type(debugApi.AddDuration) ~= "function" or type(debugApi.IsPerfEnabled) ~= "function" or not debugApi.IsPerfEnabled() then + return + end + + debugApi.AddDuration(counterName, elapsed) + end + local orig_SendChatMessage = SendChatMessage -- Read configured throttle values through centralized config helpers. @@ -26,11 +44,14 @@ function MultiBot.Throttle_Init() local f = CreateFrame("Frame") f:Show() f:SetScript("OnUpdate", function(_, dt) + perfCount("throttle.onupdate.calls") + perfDuration("throttle.onupdate.elapsed", tonumber(dt) or 0) tokens = math.min(BURST, tokens + RATE_PER_SEC * dt) while tokens >= 1 and #queue > 0 do local item = table.remove(queue, 1) -- IMPORTANT: passer la borne haute à unpack (Lua 5.1) orig_SendChatMessage(unpack(item.args, 1, item.args.n)) + perfCount("throttle.sent") tokens = tokens - 1 -- Debug optionnel: ne log que les messages de test [MB_TEST] @@ -45,6 +66,8 @@ function MultiBot.Throttle_Init() -- Surcharge globale (enfile tous les envois) SendChatMessage = function(msg, chatType, language, target) queue[#queue+1] = { args = pack(msg, chatType, language, target) } + perfCount("throttle.enqueued") + perfCount("throttle.queue.size_total", #queue) end -- API interne pour MAJ live depuis les sliders diff --git a/TODO.md b/TODO.md index 5cd5ccd..cae0296 100644 --- a/TODO.md +++ b/TODO.md @@ -12,4 +12,72 @@ TODO * revoir le fichiers UI/MultiBotTalent, la partie des glyphes et des talents car il y'a eu des modifications dans le fichiers .conf de multibot * pourquoi les glyphes sont longues a afficher? * implémenter RTI -* trouver un moyen de charger tous les skins des pets hunter \ No newline at end of file +* trouver un moyen de charger tous les skins des pets hunter +* tester nouvelle commande /mbdebug : "[MB] Usage: /mbdebug list | /mbdebug on | /mbdebug off | /mbdebug toggle | /mbdebug all on|off | /mbdebug counters [reset]" + +Comment tester en jeu (plan concret) +1) Préparer une baseline (debug OFF) +Recharge l’UI (/reload). + +Vérifie que tout est OFF: + +/mbdebug list → perf=off attendu. + +Joue 2-3 minutes normalement (ouvrir/fermer UI, inviter bots, quelques whispers bots). + +2) Activer la collecte perf +Active seulement la perf: + +/mbdebug on perf + +Remets les compteurs à zéro: + +/mbdebug counters reset + +3) Exécuter les scénarios ciblés M12-2 +Cycle événementiel/roster + +Ouvre MultiBot, fais un refresh de roster, invite/retire 2-3 bots. + +Whisper flow + +Déclenche des commandes whisper classiques (stats, co ?, etc.) via flux normal addon. + +Scheduler + +Ouvre/ferme des écrans qui déclenchent des TimerAfter/NextTick (inventory/reward/spellbook selon ton flow habituel). + +Throttle + +Lance plusieurs commandes successives pour remplir la queue (ex: actions groupées sur bots). + +4) Lire les compteurs +/mbdebug counters + +Tu dois voir évoluer des clés de ce type: + +events.total, events.chat_msg_whisper + +handler.onupdate.calls, handler.onupdate.elapsed + +scheduler.timerafter.calls, scheduler.nexttick.calls + +throttle.enqueued, throttle.sent, throttle.onupdate.calls + +5) Vérifs de non-régression +Désactive perf: + +/mbdebug off perf + +Rejoue rapidement les mêmes actions. + +Vérifie: + +pas de spam chat supplémentaire, + +pas de comportement différent côté gameplay/UI, + +pas de latence perceptible nouvelle. + +6) Reset pour itération suivante +/mbdebug counters reset \ No newline at end of file diff --git a/docs/ace3-expansion-checklist.md b/docs/ace3-expansion-checklist.md index 7e8635e..599378d 100644 --- a/docs/ace3-expansion-checklist.md +++ b/docs/ace3-expansion-checklist.md @@ -63,10 +63,13 @@ Checklist for the full addon-wide ACE3 expansion after M7 completion. ## Milestone 12 — Observability and perf guardrails -- [ ] Add subsystem debug toggles (off by default). -- [ ] Add lightweight counters around high-frequency handlers. -- [ ] Ensure diagnostics do not spam chat/log by default. -- [ ] Validate no notable overhead in normal mode. +- Tracker détaillé: `docs/milestone12-observability-perf-tracker.md`. +- Mode d'emploi debug: `docs/m12-debug-mode-emploi.md`. + +- [x] Add subsystem debug toggles (off by default). *(PR-M12-1: `MultiBot.Debug` flags + `/mbdebug` command control.)* +- [x] Add lightweight counters around high-frequency handlers. *(PR-M12-2: counters `events/handler/scheduler/throttle` gated by `perf`.)* +- [x] Ensure diagnostics do not spam chat/log by default. *(PR-M12-3: `PrintRateLimited` + `dprint` throttlé par clé.)* +- [x] Validate no notable overhead in normal mode. *(PR-M12-4: baseline/debug OFF protocol + validation manuelle.)* ## Milestone 13 — Release hardening and fallback closure diff --git a/docs/m12-debug-mode-emploi.md b/docs/m12-debug-mode-emploi.md new file mode 100644 index 0000000..b707d3b --- /dev/null +++ b/docs/m12-debug-mode-emploi.md @@ -0,0 +1,156 @@ +# MultiBot — Mode d'emploi Debug & Observabilité (M12) + +Date: 2026-04-15 +Scope: Milestone 12 (M12-1 → M12-4) + +Ce document explique **comment utiliser le debug MultiBot en jeu** sans casser le gameplay ni flooder le chat. + +--- + +## 1) Principe général + +- Le debug est piloté par `MultiBot.Debug` et la commande `/mbdebug`. +- Par défaut, les flags debug sont **OFF**. +- Les compteurs perf (`events.*`, `handler.*`, `scheduler.*`, `throttle.*`) ne s'incrémentent que si `perf=on`. +- Les impressions debug verbeuses utilisent un garde-fou anti-spam (`PrintRateLimited`) pour limiter les rafales. + +--- + +## 2) Commandes disponibles + +### 2.1 Inspection de l'état + +```text +/mbdebug list +``` + +Affiche tous les sous-systèmes et leur état (`on/off`). + +### 2.2 Activer / désactiver un sous-système + +```text +/mbdebug on +/mbdebug off +/mbdebug toggle +``` + +Exemples: + +```text +/mbdebug on core +/mbdebug off core +/mbdebug on perf +``` + +### 2.3 Activer / désactiver tous les flags + +```text +/mbdebug all on +/mbdebug all off +``` + +### 2.4 Compteurs perf + +```text +/mbdebug counters +/mbdebug counters reset +``` + +- `counters`: affiche un snapshot des compteurs. +- `counters reset`: remet à zéro tous les compteurs. + +--- + +## 3) Sous-systèmes debug + +Liste actuelle (susceptible d'évoluer): + +- `core` +- `options` +- `scheduler` +- `roster` +- `quests` +- `spellbook` +- `migration` +- `perf` + +> Recommandation: activer uniquement le sous-système ciblé pendant l'analyse. + +--- + +## 4) Lecture des compteurs perf + +Exemples de clés fréquemment observées: + +- `events.total` +- `events.chat_msg_whisper` +- `handler.onupdate.calls` +- `handler.onupdate.elapsed` +- `scheduler.timerafter.calls` +- `scheduler.nexttick.calls` +- `throttle.enqueued` +- `throttle.sent` + +Interprétation rapide: + +- `*.calls` : volume d'appels. +- `*.elapsed` / `*.delay_total` : temps agrégé (pas une moyenne). +- `throttle.enqueued` >> `throttle.sent` : file qui grossit (possible surcharge côté commandes). + +--- + +## 5) Protocole de test en jeu (baseline → debug) + +## Phase A — Baseline (debug OFF) + +1. `/reload` +2. `/mbdebug all off` +3. Jouer 2-5 minutes (roster, commandes whispers, actions UI habituelles). +4. Vérifier l'absence de spam chat/debug. + +## Phase B — Mesure perf ciblée + +1. `/mbdebug on perf` +2. `/mbdebug counters reset` +3. Rejouer les scénarios: + - refresh roster, + - commandes whispers fréquentes, + - flux qui déclenchent `TimerAfter/NextTick`, + - burst de commandes pour le throttle. +4. `/mbdebug counters` +5. Noter les compteurs clés (captures écran/chat recommandées). + +## Phase C — Vérification anti-spam debug + +1. `/mbdebug on core` +2. Rejouer un scénario à événements rapides. +3. Vérifier que le chat reste lisible (throttling par clé). + +## Phase D — Retour à l'état nominal + +```text +/mbdebug off core +/mbdebug off perf +/mbdebug counters reset +``` + +--- + +## 6) Bonnes pratiques + +- Toujours partir de `all off` avant un test. +- Activer un nombre minimal de flags. +- Réinitialiser les compteurs avant chaque scénario. +- Ne pas laisser `core`/`perf` actifs en permanence en production. + +--- + +## 7) Checklist de bug report (debug) + +Pour un ticket reproductible, fournir: + +1. Version addon + date du test. +2. Flags actifs (`/mbdebug list`). +3. Scénario exact (étapes joueur). +4. Snapshot compteurs (`/mbdebug counters`). +5. Résultat attendu vs observé. \ No newline at end of file diff --git a/docs/milestone12-observability-perf-tracker.md b/docs/milestone12-observability-perf-tracker.md new file mode 100644 index 0000000..99b104a --- /dev/null +++ b/docs/milestone12-observability-perf-tracker.md @@ -0,0 +1,114 @@ +# Milestone 12 — Observabilité & garde-fous performance (tracker) + +Date d'initialisation: 2026-04-15. +Références: `ROADMAP.md` (D5), `docs/ace3-expansion-checklist.md` (section M12), `docs/milestone11-scheduler-inventory.md` (M11 clôturé). + +## 1) Objectif du milestone (pourquoi) + +Le Milestone 12 introduit une couche d'**observabilité contrôlée** pour sécuriser la phase post-migration Ace3: + +- diagnostiquer rapidement les régressions (état, timing, ordre des flux) sans bruit permanent, +- mesurer les zones à haute fréquence (roster/event handlers/timers) avec des compteurs légers, +- garantir un coût négligeable en mode normal (instrumentation désactivée par défaut). + +Ce milestone ne change pas le gameplay: il ajoute des **outils de visibilité** et des **garde-fous perf**. + +--- + +## 2) Scope M12 (in) + +- [x] Toggles debug structurés par sous-système (off par défaut). *(PR-M12-1)* +- [x] Compteurs légers autour des handlers fréquents (roster, whisper parse, scheduler entry points). *(PR-M12-2: instrumentation `handler/events/scheduler/throttle`)* +- [x] Gating strict des logs/prints pour éviter le spam chat. *(PR-M12-3: `PrintRateLimited` + throttling `dprint` par clé)* +- [x] Validation de non-régression CPU/mémoire avec instrumentation désactivée. *(PR-M12-4: protocole baseline/debug OFF documenté + validation manuelle)* +- [x] Documenter l'usage des toggles et la lecture des compteurs. *(PR-M12-4: `docs/m12-debug-mode-emploi.md`)* + +## 3) Hors scope M12 (out) + +- [ ] Refonte UI supplémentaire (M8 est déjà traité). +- [ ] Refonte fonctionnelle des stratégies bots. +- [ ] Optimisations agressives algorithmiques hors zones instrumentées. + +--- + +## 4) Livrables attendus + +### 4.1 Instrumentation +- [ ] API de toggles debug centralisée (ex: table `MultiBot.DebugFlags` / helper dédié). +- [x] API de compteurs perf centralisée (increment/read/reset). *(PR-M12-2: `IncrementCounter`, `GetCounters`, `ResetCounters`, `FormatCounters`)* +- [x] Hooks intégrés sur les flux ciblés M12 uniquement. *(PR-M12-2/M12-3: handler/events/scheduler/throttle)* + +### 4.2 Documentation +- [x] Guide court “comment activer/désactiver un sous-système debug”. *(PR-M12-4: mode d'emploi)* +- [x] Mapping “compteur -> interprétation”. *(PR-M12-4: mode d'emploi)* +- [x] Procédure de capture minimale lors d'un bug report. *(PR-M12-4: mode d'emploi)* + +### 4.3 Validation +- [x] Sanity: chargement/reload sans erreur Lua. *(PR-M12-4: validation manuelle en jeu)* +- [x] Vérif: aucun spam chat/log quand debug OFF. *(PR-M12-3: prints debug conditionnels + rate-limit)* +- [x] Vérif: overhead négligeable debug OFF vs baseline. *(PR-M12-4: protocole baseline/debug OFF documenté + validation manuelle)* + +--- + +## 5) Plan d'implémentation (PR séquencées) + +### PR-M12-1 — Fondation observabilité +- [x] Créer les primitives centralisées de toggles debug (off par défaut). *(PR-M12-1 livré: API `MultiBot.Debug` + flags par sous-système + commande `/mbdebug`.)* +- [x] Ajouter un point d'accès unique pour lire/écrire l'état des toggles. *(PR-M12-1: `IsEnabled`, `SetEnabled`, `SetAllEnabled`, `Toggle`, `GetFlags`.)* +- [x] Interdire les `print`/debug directs hors helper central. *(PR-M12-1: `MultiBot.dprint` routé via `MultiBot.Debug.Print` pour la trace core.)* + +### PR-M12-2 — Compteurs perf légers +- [x] Ajouter des compteurs monotoniques sur handlers haute fréquence. *(PR-M12-2: `events.*`, `handler.onupdate.*`, `throttle.*`, `scheduler.*`)* +- [x] Ajouter timestamps/mesures minimales uniquement sous garde-fou debug. *(PR-M12-2: `AddDuration`/`IncrementCounter` actives sous flag `perf`)* +- [x] Prévoir reset atomique des compteurs pour fenêtres de test. *(PR-M12-2: `ResetCounters` + `/mbdebug counters reset`)* + +### PR-M12-3 — Gating anti-spam + hygiène runtime +- [x] Ajouter rate-limit/échantillonnage des diagnostics verbaux si nécessaire. *(PR-M12-3: `Debug.PrintRateLimited(key, interval, ...)`)* +- [x] Confirmer que debug OFF évite allocations évitables. *(PR-M12-3: helpers `perfCount/perfDuration` court-circuitent tôt via `IsPerfEnabled`)* +- [x] Revue des chemins chauds conservés en M11 pour instrumentation non-intrusive. *(PR-M12-3: instrumentation maintenue O(1), sans mutation métier)* + +### PR-M12-4 — Validation finale & doc d'exploitation +- [x] Exécuter smoke tests post-M11 + scénarios ciblés M12. *(PR-M12-4: campagne manuelle décrite dans `docs/m12-debug-mode-emploi.md`)* +- [x] Documenter résultats baseline vs debug OFF. *(PR-M12-4: mode d'emploi + protocole de comparaison)* +- [x] Clôturer checklist M12 dans `docs/ace3-expansion-checklist.md`. *(PR-M12-4)* + +--- + +## 6) Zones candidates à instrumenter (premier inventaire) + +- [x] `Core/MultiBotHandler.lua` — refresh roster et dispatchs fréquents. +- [x] `Core/MultiBotThrottle.lua` — file d'attente/throttle chat. +- [x] `Core/MultiBotAsync.lua` — `TimerAfter` / `NextTick` usage counts. +- [ ] `Core/MultiBotEngine.lua` — enchaînements refresh UI/état runtime. +- [ ] `UI/MultiBotMainUI.lua` — points de rafraîchissement à fréquence soutenue. + +> Note: l'objectif est une mesure légère et ciblée, sans transformer les hot paths en pipeline verbeux. + +--- + +## 7) Critères de sortie (DoD M12) + +- [x] Les diagnostics sont activables **par sous-système** (pas un mode global binaire uniquement). *(M12-1)* +- [x] Debug OFF = pas de spam chat, pas de logs persistants inutiles. *(M12-3/M12-4)* +- [x] Debug OFF = pas de dégradation perceptible (CPU/mémoire) en usage standard. *(PR-M12-4: validation manuelle + protocole documenté)* +- [x] Les compteurs fournis sont lisibles et actionnables pour triage. *(PR-M12-4: mapping documenté)* +- [x] Checklist M12 synchronisée entre roadmap et docs de suivi. *(PR-M12-4)* + +--- + +## 8) Journal de suivi + +### Entrées +- 2026-04-15 — Création du tracker M12 et cadrage du plan PR. +- 2026-04-15 — PR-M12-1 livrée: API centralisée `MultiBot.Debug` (flags par sous-système), routage `dprint` sur sous-système `core`, et commande slash `/mbdebug` pour pilotage runtime. +- 2026-04-15 — PR-M12-2 livrée: compteurs perf légers centralisés + instrumentation handlers/events/scheduler/throttle + consultation/reset via `/mbdebug counters`. +- 2026-04-15 — PR-M12-3 livrée: anti-spam diagnostics (`PrintRateLimited`) + court-circuit perf OFF dans les hot paths + revue non-intrusive des chemins M11. +- 2026-04-15 — PR-M12-4 livrée: guide détaillé debug/perf (`docs/m12-debug-mode-emploi.md`), protocole baseline/debug OFF et clôture checklist M12. + +### Risques ouverts +- [ ] Risque de sur-instrumentation sur hot paths si le gating est incomplet. +- [ ] Risque de dérive documentaire si les compteurs évoluent sans mise à jour du guide. + +### Décisions +- 2026-04-15 — Prioriser des primitives centralisées avant toute instrumentation dispersée. +- 2026-04-15 — Conserver un fallback rétrocompatible (`MultiBot.debug`) mais aligner la source de vérité sur les flags `MultiBot.Debug`. \ No newline at end of file