local QuestieProfiler = QuestieLoader:CreateModule("Profiler") --- COMPATIBILITY --- local C_Timer = QuestieCompat.C_Timer local BackdropTemplateMixin = not QuestieCompat.Is335 and BackdropTemplateMixin QuestieProfiler.hooks = {} QuestieProfiler.alreadyHooked = {} QuestieProfiler.needsHook = {} QuestieProfiler.hookCallCount = {} QuestieProfiler.hookTimeCount = {} QuestieProfiler.lowerCaseLookup = {} QuestieProfiler.shortestName = {} QuestieProfiler.lookupToHook = {} QuestieProfiler.hooks = {} QuestieProfiler.needsHookCount = 0 QuestieProfiler.finishedHookCount = 0 QuestieProfiler.hookedFunctionCount = 0 QuestieProfiler.highestMS = 0 QuestieProfiler.highestCalls = 0 QuestieProfiler.searchFilter = nil QuestieProfiler.condensedMode = false QuestieProfiler.scriptsToWatch = { "OnUpdate", "OnShow", "OnHide" } function QuestieProfiler:HookFunction(key, val, table, name) local lookupKey = name .. "." .. tostring(key) QuestieProfiler.shortestName[val] = lookupKey QuestieProfiler.alreadyHooked[val] = true QuestieProfiler.lookupToHook[lookupKey] = val local hook = {} hook.original = val hook.originalKey = key hook.originalParent = table QuestieProfiler.hookCallCount[lookupKey] = 0 QuestieProfiler.hookTimeCount[lookupKey] = 0 hook.override = function(...) --local _, _, name = strsplit("\\", debugstack(2)) --print(name) --print("Hookboy run " .. name .. "->"..key) local htc = QuestieProfiler.hookCallCount[lookupKey] + 1 QuestieProfiler.hookCallCount[lookupKey] = htc if htc > QuestieProfiler.highestCalls then QuestieProfiler.highestCalls = htc end local start = debugprofilestop() local ret = { hook.original(...) } start = debugprofilestop() - start htc = QuestieProfiler.hookTimeCount[lookupKey] + start QuestieProfiler.hookTimeCount[lookupKey] = htc if htc > QuestieProfiler.highestMS then QuestieProfiler.highestMS = htc end return unpack(ret) end QuestieProfiler.lowerCaseLookup[lookupKey] = string.lower(lookupKey) tinsert(QuestieProfiler.hooks, hook) table[key] = hook.override QuestieProfiler.hookedFunctionCount = QuestieProfiler.hookedFunctionCount + 1 end function QuestieProfiler:HookTable(table, name) QuestieProfiler.alreadyHooked[table] = true for key, val in pairs(table) do if QuestieProfiler.alreadyHooked[val] then if QuestieProfiler.shortestName[val] then local lookupKey if type(key) == "table" then lookupKey = name .. ".(key)" .. tostring(key) else lookupKey = name .. "." .. tostring(key) end if string.len(lookupKey) < string.len(QuestieProfiler.shortestName[val]) then QuestieProfiler.shortestName[val] = lookupKey QuestieProfiler.lowerCaseLookup[lookupKey] = string.lower(lookupKey) --print("Shorter name: " .. lookupKey) end end else --QuestieProfiler.alreadyHooked[val] = true local typ = type(val) if type(key) == "table" then tinsert(QuestieProfiler.needsHook, { key, name .. ".(key)" .. tostring(key) }) QuestieProfiler.needsHookCount = QuestieProfiler.needsHookCount + 1 end if typ == "function" then if key == "GetScript" then -- get all scripts for _, script in pairs(QuestieProfiler.scriptsToWatch) do local res, ret = pcall(val, table, script) if res then QuestieProfiler:HookScript(script, ret, table, name) end end end --print("["..QuestieProfiler.finishedHookCount.."/"..QuestieProfiler.needsHookCount.."]Hooking function " .. name .. "->" .. key) QuestieProfiler:HookFunction(key, val, table, name) elseif typ == "table" then --QuestieProfiler:HookTable(val, name .. "->"..key) tinsert(QuestieProfiler.needsHook, { val, name .. "." .. tostring(key) }) QuestieProfiler.needsHookCount = QuestieProfiler.needsHookCount + 1 end end end QuestieProfiler.finishedHookCount = QuestieProfiler.finishedHookCount + 1 end function QuestieProfiler:HookModules() for moduleName, module in pairs(QuestieLoader._modules) do print("Hooking module " .. moduleName) QuestieProfiler:HookTable(module, moduleName) end end function QuestieProfiler:HookFrames() -- bit of a hack but I want to keep questie's code 100% unaffected, this should be a stand-alone module. -- that also makes sure the profiler doesnt mess with the performance of the addon when its not running (which is almost always) --C_Timer.NewTicker(2, function() -- QuestieProfiler:HookTable(QuestieFramePool, "QuestieFramePool") -- update framepool functions --end) end function QuestieProfiler:CreateUI() local base = CreateFrame("Frame", nil, UIParent, BackdropTemplateMixin and "BackdropTemplate" or nil) base:SetFrameStrata("TOOLTIP") base:SetSize(840, 580) base:SetPoint("Center", UIParent, 0, 50) base:SetMovable(true) base:EnableMouse(true) base:RegisterForDrag("LeftButton") base:SetBackdrop({ bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background-Dark", edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", tile = true, edgeSize = 16, insets = { left = 4, right = 4, top = 4, bottom = 4 }, }) local scrollFrameTemplete if Questie.IsWotlk and (not QuestieCompat.Is335) then scrollFrameTemplete = "ScrollFrameTemplate" else scrollFrameTemplete = "UIPanelScrollFrameTemplate" end base.scrollFrame = CreateFrame("ScrollFrame", "QuestieProfilerScrollFrame", base, scrollFrameTemplete) base.scrollFrame:SetFrameStrata("TOOLTIP") base.scrollFrame:SetPoint("TOPLEFT", base, 0, -40) base.scrollFrame:SetPoint("BOTTOMRIGHT", base, 0, 30) if QuestieCompat.Is335 then base.scrollFrame.ScrollBar = _G[base.scrollFrame:GetName() .. "ScrollBar"] base.scrollFrame.ScrollBar:ClearAllPoints() base.scrollFrame.ScrollBar:SetPoint("TOPRIGHT", -6, -16) base.scrollFrame.ScrollBar:SetPoint("BOTTOMRIGHT", -6, 16) base.scrollFrame.ScrollBar.Show = function() end end base.scrollFrame.ScrollBar:Hide() base.scrollContainer = CreateFrame("Frame") base.scrollContainer:SetFrameStrata("TOOLTIP") local lineCounter = 0 base.scrollContainer.ResetLines = function() lineCounter = 0 end local lines = {} base.scrollContainer.CreateLineFrame = function() local label = base.scrollContainer:CreateFontString(nil, "ARTWORK", "GameFontNormal") label:SetText("") label:SetPoint("TOPLEFT", base.scrollContainer, 0, -lineCounter * 10) label:SetWidth(base.scrollFrame:GetWidth() - 270) label:SetJustifyH("RIGHT") local label2 = base.scrollContainer:CreateFontString(nil, "ARTWORK", "GameFontNormal") label2:SetText("") label2:SetPoint("TOPLEFT", base.scrollContainer, base.scrollFrame:GetWidth() - 260, -lineCounter * 10) local label3 = base.scrollContainer:CreateFontString(nil, "ARTWORK", "GameFontNormal") label3:SetText("") label3:SetPoint("TOPLEFT", base.scrollContainer, base.scrollFrame:GetWidth() - 150, -lineCounter * 10) base.scrollContainer:SetSize(base.scrollFrame:GetWidth(), lineCounter * 10) return { label, label2, label3 } end base.scrollContainer.GetNextLine = function() lineCounter = lineCounter + 1 if not lines[lineCounter] then lines[lineCounter] = base.scrollContainer.CreateLineFrame() end return lines[lineCounter] end base.scrollContainer:SetSize(base.scrollFrame:GetWidth(), (base.scrollFrame:GetHeight() * 2)) base.scrollFrame:SetScrollChild(base.scrollContainer) base.scrollFrame:Show() QuestieProfiler.baseUI = base -- should maybe be exposed in QuestieLib local function RGBToHex(r, g, b) if r > 255 then r = 255 end if g > 255 then g = 255 end if b > 255 then b = 255 end if r < 0 then r = 0 end if g < 0 then g = 0 end if b < 0 then b = 0 end return string.format("\124cFF%02x%02x%02x", r, g, b) end local function FloatRGBToHex(r, g, b) return RGBToHex(r * 254, g * 254, b * 254) end local function valToHex(float) if float < .49 then return FloatRGBToHex(1, 0 + float / .5, 0) end if float == .50 then return FloatRGBToHex(1, 1, 0) end if float > .50 then return FloatRGBToHex(1 - float / 2, 1, 0) end end local statusLabel = base:CreateFontString(nil, "ARTWORK", "GameFontNormal") statusLabel:SetText("This is a test.") statusLabel:SetPoint("BOTTOMLEFT", base, 10, 10) statusLabel:Show() base.lastIndex = 0 base.update = function() base.scrollContainer.ResetLines() local sorted = {} local index = 0 local timeCount = QuestieProfiler.hookTimeCount local callCount = QuestieProfiler.hookCallCount local highestMS = QuestieProfiler.highestMS local highestCalls = QuestieProfiler.highestCalls if QuestieProfiler.condensedMode then timeCount = {} callCount = {} highestMS = 0 highestCalls = 0 for call, _ in pairs(QuestieProfiler.hookTimeCount) do local callStack = {} index = 0 for token in string.gmatch(call, "[^.]+") do --print(token) tinsert(callStack, token) index = index + 1 end --local ncall = (callStack[index-2] or "?") .. "." .. (callStack[index-1] or "?") .. ":" .. callStack[index].."()" --local ncall = (callStack[index-1] or "?") .. ":" .. callStack[index].."()" local ncall = callStack[index] .. "()" if callStack[index - 1] and not tonumber(callStack[index - 1]) then ncall = callStack[index - 1] .. "." .. ncall end if callStack[index - 2] and not tonumber(callStack[index - 2]) then ncall = callStack[index - 2] .. "." .. ncall end if not timeCount[ncall] then timeCount[ncall] = 0 callCount[ncall] = 0 end local time = timeCount[ncall] + QuestieProfiler.hookTimeCount[call] local calls = callCount[ncall] + QuestieProfiler.hookCallCount[call] timeCount[ncall] = time callCount[ncall] = calls QuestieProfiler.lowerCaseLookup[ncall] = string.lower(ncall) if time > highestMS then highestMS = time end if calls > highestCalls then highestCalls = calls end end end for call, count in pairs(timeCount) do if ((count > 0 and not QuestieProfiler.searchFilter) or (callCount[call] > 0 and QuestieProfiler.searchFilter and string.match(QuestieProfiler.lowerCaseLookup[call], QuestieProfiler.searchFilter))) then tinsert(sorted, { call, count }) index = index + 1 end end table.sort(sorted, function(a, b) return a[2] > b[2] end) statusLabel:SetText(string.format("Total functions hooked: %d Highest call count: %d Highest time: %.2fms", QuestieProfiler.hookedFunctionCount, highestCalls, highestMS)) index = 0 for call in pairs(sorted) do call = sorted[call][1] local count = timeCount[call] local line = base.scrollContainer.GetNextLine() local color if mod(index, 2) == 1 then color = "\124cFFFFFFFF" --color2 = "\124cFF88FFAA" else color = "\124cFFEEEEEE" --color2 = "\124cFF77EE99" end local color2 = valToHex(1 - (count / (highestMS / 2))) or "\124cFFFF0000" local color3 = valToHex(1 - (callCount[call] / (highestCalls / 2))) or "\124cFFFF0000" --print("Highest ms: " .. QuestieProfiler.highestMS) --print("Highest calls: " .. QuestieProfiler.highestCalls) local callstr = QuestieProfiler.shortestName[QuestieProfiler.lookupToHook[call]] or call if (string.len(callstr) > 64) then callstr = "..." .. string.sub(callstr, string.len(callstr) - 64) end line[1]:SetText(color .. callstr) line[2]:SetText(color2 .. string.format("time=%.2fms", count)) line[3]:SetText(color3 .. "call count=" .. callCount[call]) index = index + 1 if index > 150 then break end end if base.lastIndex > index then for _ = index, base.lastIndex do local line = base.scrollContainer.GetNextLine() line[1]:SetText("") line[2]:SetText("") line[3]:SetText("") end end base.lastIndex = index end base.update() local button = CreateFrame("Button", nil, base) button:SetPoint("TOPLEFT", base, 10, -10) button:SetWidth(80) button:SetHeight(24) button:SetText("Reset") button:SetNormalFontObject("GameFontNormal") local function buildTexture(str) local tex = button:CreateTexture() tex:SetTexture(str) tex:SetTexCoord(0, 0.625, 0, 0.6875) tex:SetAllPoints() return tex end button:SetNormalTexture(buildTexture("Interface/Buttons/UI-Panel-Button-Up")) button:SetHighlightTexture(buildTexture("Interface/Buttons/UI-Panel-Button-Highlight")) button:SetPushedTexture(buildTexture("Interface/Buttons/UI-Panel-Button-Down")) button:SetScript("OnClick", function(self, ...) self:Disable() QuestieProfiler:Unhook() QuestieProfiler:DoHooks(function() for key in pairs(QuestieProfiler.hookCallCount) do QuestieProfiler.hookCallCount[key] = 0 QuestieProfiler.hookTimeCount[key] = 0 end QuestieProfiler.highestMS = 0 QuestieProfiler.highestCalls = 0 for _, line in pairs(lines) do line[1]:SetText("") line[2]:SetText("") line[3]:SetText("") end self:Enable() end) end) button = CreateFrame("Button", nil, base) button:SetPoint("TOPLEFT", base, base:GetWidth() - 70, -10) button:SetWidth(60) button:SetHeight(24) button:SetText("Close") button:SetNormalFontObject("GameFontNormal") button:SetNormalTexture(buildTexture("Interface/Buttons/UI-Panel-Button-Up")) button:SetHighlightTexture(buildTexture("Interface/Buttons/UI-Panel-Button-Highlight")) button:SetPushedTexture(buildTexture("Interface/Buttons/UI-Panel-Button-Down")) button:SetScript("OnClick", function() QuestieProfiler:Unhook() base:Hide() end) button = CreateFrame("Button", nil, base) button:SetPoint("TOPLEFT", base, 90, -10) button:SetWidth(140) button:SetHeight(24) button:SetText("Mode: Full") button:SetNormalFontObject("GameFontNormal") button:SetNormalTexture(buildTexture("Interface/Buttons/UI-Panel-Button-Up")) button:SetHighlightTexture(buildTexture("Interface/Buttons/UI-Panel-Button-Highlight")) button:SetPushedTexture(buildTexture("Interface/Buttons/UI-Panel-Button-Down")) button:SetScript("OnClick", function(...) QuestieProfiler.condensedMode = not QuestieProfiler.condensedMode if QuestieProfiler.condensedMode then button:SetText("Mode: Condensed") else button:SetText("Mode: Full") end for key in pairs(QuestieProfiler.hookCallCount) do QuestieProfiler.hookCallCount[key] = 0 QuestieProfiler.hookTimeCount[key] = 0 end QuestieProfiler.highestMS = 0 QuestieProfiler.highestCalls = 0 for _, line in pairs(lines) do line[1]:SetText("") line[2]:SetText("") line[3]:SetText("") end end) local searchBox = CreateFrame("Frame", nil, base, BackdropTemplateMixin and "BackdropTemplate") searchBox:SetPoint("TOPLEFT", base, 260, -10) searchBox:SetWidth(192) searchBox:SetHeight(24) searchBox:Show() searchBox:SetBackdrop({ bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background", edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", tile = true, tileSize = 0, edgeSize = 16, insets = { left = 0, right = 0, top = 0, bottom = 0 } }) local search = CreateFrame("EditBox", nil, searchBox, BackdropTemplateMixin and "BackdropTemplate" or nil) search:SetMultiLine(false) search:SetFontObject(ChatFontNormal) search:SetWidth(180) search:SetHeight(20) search:SetFrameStrata("TOOLTIP") search:SetPoint("TOPLEFT", searchBox, 6, -1) search:SetText("\124cFF888888Filter...") search:HookScript("OnMouseDown", function() if search:GetText() == "\124cFF888888Filter..." then search:SetText("") end end) local function clearFocus() search:ClearFocus() if string.len(search:GetText()) == 0 then search:SetText("\124cFF888888Filter...") end end search:SetAutoFocus(false) search:SetScript("OnEscapePressed", clearFocus) search:HookScript(QuestieCompat.Is335 and "OnTextChanged" or "OnKeyUp", function(self, userInput) local txt = string.lower(search:GetText()) if string.len(txt) == 0 or userInput == false then txt = nil end QuestieProfiler.searchFilter = txt end) base:SetScript("OnMouseDown", clearFocus) base:SetScript("OnDragStart", function() base:StartMoving() clearFocus() end) base:SetScript("OnDragStop", function() base:StopMovingOrSizing() end) search:Show() base:Show() C_Timer.NewTicker(0.5, base.update) end function QuestieProfiler:DoHooks(after) local timer -- libstub modules. TODO: check debugstack() to only include calls made by questie --QuestieProfiler.needsHookCount = QuestieProfiler.needsHookCount + 1 --tinsert(QuestieProfiler.needsHook, {LibStub, "LibStub"}) for moduleName, module in pairs(QuestieLoader._modules) do if module ~= QuestieProfiler then QuestieProfiler.needsHookCount = QuestieProfiler.needsHookCount + 1 tinsert(QuestieProfiler.needsHook, { module, moduleName }) end end timer = C_Timer.NewTicker(0.01, function() for _ = 0, 512 do local toHook = tremove(QuestieProfiler.needsHook) if toHook then QuestieProfiler:HookTable(toHook[1], toHook[2]) else timer:Cancel() if after then after() end return end end end) end function QuestieProfiler:Unhook() for _, hook in pairs(QuestieProfiler.hooks) do hook.originalParent[hook.originalKey] = hook.original end QuestieProfiler.hooks = {} QuestieProfiler.alreadyHooked = {} QuestieProfiler.needsHookCount = 0 QuestieProfiler.finishedHookCount = 0 QuestieProfiler.hookedFunctionCount = 0 QuestieProfiler.highestMS = 0 QuestieProfiler.highestCalls = 0 end function QuestieProfiler:Start() -- call ingame when developing /run QuestieLoader:ImportModule("Profiler"):Start() print("Starting profiler...") QuestieProfiler:DoHooks(function() QuestieProfiler:HookFrames() if QuestieProfiler.baseUI then QuestieProfiler.baseUI:Show() else QuestieProfiler:CreateUI() end end) end