Module:Sandbox/Aidan9382/Benchmarker
![]() | This module is rated as alpha. It is ready for third-party input, and may be used on a few pages to see if problems arise, but should be watched. Suggestions for new features or changes in their input and output mechanisms are welcome. |
A module benchmarker aimed at benchmarking at a wider scope and reliably. This differs from the standard MediaWiki Lua Profile in that it profiles specifically exported module functions instead of every function (e.g. standard string library functions). This lets you figure out what module is making expensive calls rather than what expensive calls are being made.
Usage
[edit]To benchmark a module and all submodules it calls, put local __prepare = require("Module:Sandbox/Aidan9382/Benchmarker")
on the first line and __prepare(p, "<Module name>")
(or the equivilant to p) at the very bottom just before the return in the highest level module you want to benchmark from. You do not need to include the benchmarker in any other modules; any module fetched using require
will automatically have its returned value hooked.
Example output
[edit]-- Benchmarker Finished -- Total time taken: 91.1ms Top 5 modules by time taken: Module:Pagetype/sandbox: 55.7ms (61.1%) Module:Wikitext Parsing: 25.5ms (28%) Module:WikiProject banner/sandbox: 7.8ms (8.6%) Module:Template parameter value: 1.9ms (2.1%) Module:Arguments: 0.2ms (0.2%) Top 5 functions by time taken: Module:Pagetype/sandbox._main: 55.7ms (61.1%) Module:Wikitext Parsing.PrepareText: 25.5ms (28%) Module:WikiProject banner/sandbox._main: 7.4ms (8.1%) Module:Template parameter value.getValue: 1ms (1.1%) Module:Template parameter value.getTemplate: 1ms (1.1%)
-- In-depth execution speed benchmarker - read the /doc for more info
-- =================================================================== --
-- This is a meta-module that hooks globals, which could be disruptive --
-- Be careful including this module outside of sandboxes --
-- =================================================================== --
-- Always use rawget/rawset on _G to bypass strict
local ActiveHooker = rawget(_G, "_BenchmarkerHooker")
if ActiveHooker ~= nil then
return ActiveHooker
end
--== Personal stuff ==--
local function dp(x, n)
n = n or 4
return math.floor(x*10^n+0.5) / 10^n
end
local function GetVarargInfo(...)
return {...}, select("#", ...)
end
local function DetermineCaller(stacktrace)
for line in stacktrace:gmatch("[^\n]+") do
if not line:find("^stack traceback:") and not line:find("Aidan9382/Benchmarker") then
local f, l = line:match("^%s*([^:]+):([^:]+)")
return {Function=f, Line=tonumber(l)}
end
end
end
local CompleteCalls = {}
local FunctionCallStack = {}
local NoHookZone = {}
local function FinishUp()
-- Note: Don't currently use caller stats. Eh, whatever
local TotalTimeTaken = 0
local ModuleTotalTimes = {}
local FunctionTotalTimes = {}
local SeenModules = {}
local SeenFunctions = {}
for _, Call in next, CompleteCalls do
local CallTime = Call.TimeTaken - Call.Offset
TotalTimeTaken = TotalTimeTaken + CallTime
if not ModuleTotalTimes[Call.Origin] then
ModuleTotalTimes[Call.Origin] = 0
SeenModules[#SeenModules+1] = Call.Origin
end
ModuleTotalTimes[Call.Origin] = ModuleTotalTimes[Call.Origin] + CallTime
local UniqueName = Call.Origin .. "." .. Call.Name
if not FunctionTotalTimes[UniqueName] then
FunctionTotalTimes[UniqueName] = 0
SeenFunctions[#SeenFunctions+1] = UniqueName
end
FunctionTotalTimes[UniqueName] = FunctionTotalTimes[UniqueName] + CallTime
end
if TotalTimeTaken > .01 then
table.sort(SeenModules, function(a, b)
return ModuleTotalTimes[a] > ModuleTotalTimes[b]
end)
table.sort(SeenFunctions, function(a, b)
return FunctionTotalTimes[a] > FunctionTotalTimes[b]
end)
mw.log("\n-- Benchmarker Finished --")
mw.log("Total time taken: " .. dp(TotalTimeTaken)*1000 .. "ms")
mw.log("\nTop 5 modules by time taken:")
for i = 1, math.min(5, #SeenModules) do
local t = dp(ModuleTotalTimes[SeenModules[i]])
mw.log(SeenModules[i] .. ": " .. t*1000 .. "ms (" .. dp(t/TotalTimeTaken, 3)*100 .. "%)")
end
mw.log("\nTop 5 functions by time taken:")
for i = 1, math.min(5, #SeenFunctions) do
local t = dp(FunctionTotalTimes[SeenFunctions[i]])
mw.log(SeenFunctions[i] .. ": " .. t*1000 .. "ms (" .. dp(t/TotalTimeTaken, 3)*100 .. "%)")
end
mw.log("") -- extra newline
end
CompleteCalls = {}
end
local function HookFunction(f, fname, origin)
if not NoHookZone[f] then
local out = function(...)
local callerinfo = DetermineCaller(debug.traceback())
local StackObject = {
Name=fname, Origin=origin, Offset=0,
Caller=callerinfo.Function, CallLine=callerinfo.Line
}
FunctionCallStack[#FunctionCallStack+1] = StackObject
local s = os.clock()
local response, length = GetVarargInfo(f(...))
local timetaken = os.clock() - s
StackObject.TimeTaken = timetaken
CompleteCalls[#CompleteCalls+1] = StackObject
local maxi = #FunctionCallStack
FunctionCallStack[maxi] = nil
if maxi == 1 then
FinishUp()
else
FunctionCallStack[maxi-1].Offset = FunctionCallStack[maxi-1].Offset + timetaken
end
return unpack(response, 1, length)
end
NoHookZone[out] = true
return out
else
return f
end
end
local function HookTable(obj, origin)
-- safety catch since we export this function
if type(obj) == "function" then
return HookFunction(obj, "<main>", origin)
end
for a, b in next, obj do
if type(b) == "function" then
obj[a] = HookFunction(b, a, origin)
end
end
return obj
end
rawset(_G, "_BenchmarkerHooker", HookTable)
--== Global hooking ==--
local require = require
local function hookedrequire(source)
local out = require(source)
if source ~= "strict" and source ~= "Module:Sandbox/Aidan9382/Benchmarker" then
if type(out) == "table" then
HookTable(out, source)
elseif type(out) == "function" then
out = HookFunction(out, "<main>", source)
end
end
return out
end
rawset(_G, "require", hookedrequire)
return HookTable