Module:Sandbox/User:Waddie96/benchmark
Appearance
-- __NOINDEX__
--- Lua benchmarks: Compare performance of two functions over CPU time
--- [[Module:Benchmark]]
local ITERATIONS = 1e6 -- Default number of iterations
local p = {}
local _format = string.format
local html_create = mw.html.create
local _sub = string.sub
local _byte = string.byte
local _hash = mw.hash.hashValue
local _tostring = tostring
--[[ local input = ' "nowiki", ">", "D", "</", "nowiki", "> |<", "pre", ">", "E|F", "</", '
local whitespace = { [" "] = 1, ["\n"] = 1, ["\t"] = 1, ["\r"] = 1 }
local strlen = #input
local function func1()
-- local out = string.gsub(input, "^%s*(.-)%s*$", "%1")
local lowEnd
for i = 1, strlen do
if not whitespace[_sub(input, i, i)] then
lowEnd = i
break
end
end
if not lowEnd then
return ""
end
for i = strlen, 1, -1 do
if not whitespace[_sub(input, i, i)] then
local out = _sub(input, lowEnd, i)
return out
end
end
return out
end
local function func2()
local low, high = 1, strlen
-- find first non-whitespace
while low <= strlen and whitespace[_byte(input, low)] do
low = low + 1
end
-- if all whitespace
if low > strlen then return "" end
-- find last non-whitespace
while high >= low and whitespace[_byte(input, high)] do
high = high - 1
end
return _sub(input, low, high)
end ]]
local to_number = tonumber
local _type = type
local _lower = string.lower
local function func1(val)
-- If your wiki uses non-ascii characters for any of "yes", "no", etc., you
-- should replace "val:lower()" with "mw.ustring.lower(val)" in the
-- following line.
val = _type(val) == 'string' and _lower(val) or val
if val == nil then
return nil
elseif val == true
or val == 'yes'
or val == 'y'
or val == 'true'
or val == 't'
or val == 'on'
or to_number(val) == 1
then
return true
elseif val == false
or val == 'no'
or val == 'n'
or val == 'false'
or val == 'f'
or val == 'off'
or to_number(val) == 0
then
return false
else
return default
end
end
local LOWER = string.lower
local TO_NUMBER = tonumber
local TYPE = type
local BOOLEAN_MAP = {
yes = true,
y = true,
["true"] = true,
t = true,
on = true,
["1"] = true,
no = false,
n = false,
["false"] = false,
f = false,
off = false,
["0"] = false,
}
local function func2(value)
if value == nil then
return nil
end
local valueType = TYPE(value)
if valueType == 'boolean' then
return value
elseif valueType == 'string' then
local lookupResult = BOOLEAN_MAP[LOWER(value)]
if lookupResult ~= nil then
return lookupResult
end
end
-- Numeric check works for both numbers and numeric strings.
local number = TO_NUMBER(value)
if number == 1 then
return true
elseif number == 0 then
return false
end
return defaultResponse
end
--- Format seconds into readable text
--- @param seconds number
--- @return string
local function format_time(seconds)
if seconds < 0.001 then
-- less than 1 millisecond → show in microseconds
return _format("%.3f µs", seconds * 1e6)
elseif seconds < 0.1 then
-- between 1 ms and 100 ms → show in milliseconds
return _format("%.3f ms", seconds * 1e3)
else
-- otherwise show seconds
return _format("%.6f s", seconds)
end
end
--- Run a function multiple times and measure total CPU time
--- @param iterations integer Number of iterations
--- @return number elapsed1 CPU time in seconds
--- @return number elapsed2 CPU time in seconds
--- @return integer correct
--- @return integer incorrect
local function measure(iterations)
local out1, out2 = {}, {}
local correct, incorrect = 0, 0
local start1 = os.clock()
for _ = 1, iterations do
table.insert(out1, _hash('xxh64', _tostring(func1(1))))
end
local stop1 = os.clock()
local start2 = os.clock()
for _ = 1, iterations do
table.insert(out2, _hash('xxh64', _tostring(func2(1))))
end
local stop2 = os.clock()
for i = 1, iterations do
if out1[i] == out2[i] and out1[i] ~= nil and out2[i] ~= nil then
correct = correct + 1
else
incorrect = incorrect + 1
end
end
return (stop1 - start1), (stop2 - start2), correct, incorrect, out1[1], out2[1]
end
local function make_table(t1, t2, n, faster, percent, correct, incorrect, out1, out2)
local num = n
n = mw.getCurrentFrame():callParserFunction( "formatnum", n )
local tbl = html_create("table")
:addClass("wikitable")
:cssText("text-align: center;")
:node(
html_create("tr")
:node(html_create("th"):wikitext("Function"))
:done()
:node(html_create("th"):wikitext("Total time"))
:done()
:node(html_create("th"):wikitext("Average time per iteration"))
:done()
)
:done()
:node(
html_create("tr")
:node(html_create("td"):wikitext("Function 1"))
:done()
:node(html_create("td"):wikitext(format_time(t1)))
:done()
:node(html_create("td"):wikitext(format_time(t1 / num)))
:done()
)
:done()
:node(
html_create("tr")
:node(html_create("td"):wikitext("Function 2"))
:done()
:node(html_create("td"):wikitext(format_time(t2)))
:done()
:node(html_create("td"):wikitext(format_time(t2 / num)))
:done()
)
:done()
:node(
html_create("tr")
:node(
html_create("td"):cssText("font-weight: bold;"):wikitext('Comparison')
)
:done()
:node(
html_create("td"):attr("colspan", "2"):wikitext(
_format("Function %s is <strong>%.2f%% faster</strong>", faster, percent)
)
)
:done()
)
:done()
:node(
html_create("tr")
:node(
html_create("td"):cssText("font-weight: bold;"):wikitext('Iterations')
)
:done()
:node(
html_create("td"):attr("colspan", "2"):wikitext(
mw.getCurrentFrame()
:callParserFunction( "formatnum", n )
)
)
:done()
)
:done()
:node(
html_create("tr")
:node(
html_create("td"):cssText("font-weight: bold;"):wikitext("Correct")
)
:done()
:node(
html_create("td"):attr("colspan", "2"):wikitext(
mw.getCurrentFrame()
:callParserFunction( "formatnum", correct )
)
)
:done()
)
:done()
:node(
html_create("tr")
:node(
html_create("td"):cssText("font-weight: bold;"):wikitext('Incorrect')
)
:done()
:node(
html_create("td"):attr("colspan", "2"):wikitext(
mw.getCurrentFrame()
:callParserFunction( "formatnum", incorrect )
)
)
:done()
)
:done()
:allDone()
out1 = out1 or 'nil'
out2 = out2 or 'nil'
tbl_str = mw.allToString(tbl) .. '<strong>Output 1:</strong> ' .. func1() ..
'<br/> <strong>Output 2:</strong> ' .. func2() ..
'<br/> <strong>Output 1:</strong>' .. _tostring(out1) ..
'<br/> <strong>Output 2:</strong>' .. _tostring(out2)
return tbl_str
end
--- Main benchmark function
--- @param iterations? integer Number of iterations (default 10000)
--- @return string Wikitext table comparing results
function p.run(iterations)
local totalstart = os.clock()
local n = tonumber(iterations) or ITERATIONS
local t1, t2, correct, incorrect, out1, out2 = measure(n)
local faster = t1 < t2 and "1" or "2"
local diff = math.abs(t1 - t2)
local percent = (diff / math.max(t1, t2)) * 100
local out = make_table(t1, t2, n, faster, percent, correct, incorrect, out1, out2)
local totalend = os.clock()
local totalsec = totalend - totalstart
out = out .. '<br/> <strong>Total time taken:</strong> ' .. format_time(totalsec)
return out
end
return p