Jump to content

Module:Sandbox/Aidan9382/Benchmarker

From Wikipedia, the free encyclopedia

-- 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