trac-inf.lua / last modification: 2020-01-30 14:16
if not modules then modules = { } end modules ['trac-inf'] = {
    version   = 1.001,
    comment   = "companion to trac-inf.mkiv",
    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
    copyright = "PRAGMA ADE / ConTeXt Development Team",
    license   = "see context related readme files"
}

-- As we want to protect the global tables, we no longer store the timing
-- in the tables themselves but in a hidden timers table so that we don't
-- get warnings about assignments. This is more efficient than using rawset
-- and rawget.

local type, tonumber, select = type, tonumber, select
local format, lower, find = string.format, string.lower, string.find
local concat = table.concat
local clock  = os.gettimeofday or os.clock -- should go in environment

local setmetatableindex = table.setmetatableindex
local serialize         = table.serialize
local formatters        = string.formatters

statistics              = statistics or { }
local statistics        = statistics

statistics.enable       = true
statistics.threshold    = 0.01

local statusinfo, n, registered, timers = { }, 0, { }, { }

setmetatableindex(timers,function(t,k)
    local v = { timing = 0, loadtime = 0, offset = 0 }
    t[k] = v
    return v
end)

local function hastiming(instance)
    return instance and timers[instance]
end

local function resettiming(instance)
    timers[instance or "notimer"] = { timing = 0, loadtime = 0, offset = 0 }
end

local ticks   = clock
local seconds = function(n) return n or 0 end

-- if FFISUPPORTED and ffi and os.type == "windows" then
--
--     local okay, kernel = pcall(ffi.load,"kernel32")
--
--     if kernel then
--
--         local tonumber = ffi.number or tonumber
--
--         ffi.cdef[[
--             int QueryPerformanceFrequency(int64_t *lpFrequency);
--             int QueryPerformanceCounter(int64_t *lpPerformanceCount);
--         ]]
--
--         local target = ffi.new("__int64[1]")
--
--         ticks = function()
--             if kernel.QueryPerformanceCounter(target) == 1 then
--                 return tonumber(target[0])
--             else
--                 return 0
--             end
--         end
--
--         local target = ffi.new("__int64[1]")
--
--         seconds = function(ticks)
--             if kernel.QueryPerformanceFrequency(target) == 1 then
--                 return ticks / tonumber(target[0])
--             else
--                 return 0
--             end
--         end
--
--     end
--
-- end

local function starttiming(instance,reset)
    local timer = timers[instance or "notimer"]
    local it = timer.timing
    if reset then
        it = 0
        timer.loadtime = 0
    end
    if it == 0 then
        timer.starttime = ticks()
        if not timer.loadtime then
            timer.loadtime = 0
        end
    end
    timer.timing = it + 1
end

local function stoptiming(instance)
    local timer = timers[instance or "notimer"]
    local it = timer.timing
    if it > 1 then
        timer.timing = it - 1
    else
        local starttime = timer.starttime
        if starttime and starttime > 0 then
            local stoptime  = ticks()
            local loadtime  = stoptime - starttime
            timer.stoptime  = stoptime
            timer.loadtime  = timer.loadtime + loadtime
            timer.timing    = 0
            timer.starttime = 0
            return loadtime
        end
    end
    return 0
end

local function benchmarktimer(instance)
    local timer = timers[instance or "notimer"]
    local it = timer.timing
    if it > 1 then
        timer.timing = it - 1
    else
        local starttime = timer.starttime
        if starttime and starttime > 0 then
            timer.offset = ticks() - starttime
        else
            timer.offset = 0
        end
    end
end

local function elapsed(instance)
    if type(instance) == "number" then
        return instance
    else
        local timer = timers[instance or "notimer"]
        return timer and seconds(timer.loadtime - 2*(timer.offset or 0)) or 0
    end
end

local function currenttime(instance)
    if type(instance) == "number" then
        return instance
    else
        local timer = timers[instance or "notimer"]
        local it = timer.timing
        if it > 1 then
            -- whatever
        else
            local starttime = timer.starttime
            if starttime and starttime > 0 then
                return seconds(timer.loadtime + ticks() - starttime -  2*(timer.offset or 0))
            end
        end
        return 0
    end
end

local function elapsedtime(instance)
    return format("%0.3f",elapsed(instance))
end

local function elapsedindeed(instance)
    return elapsed(instance) > statistics.threshold
end

local function elapsedseconds(instance,rest) -- returns nil if 0 seconds
    if elapsedindeed(instance) then
        return format("%0.3f seconds %s", elapsed(instance),rest or "")
    end
end

statistics.hastiming      = hastiming
statistics.resettiming    = resettiming
statistics.starttiming    = starttiming
statistics.stoptiming     = stoptiming
statistics.currenttime    = currenttime
statistics.elapsed        = elapsed
statistics.elapsedtime    = elapsedtime
statistics.elapsedindeed  = elapsedindeed
statistics.elapsedseconds = elapsedseconds
statistics.benchmarktimer = benchmarktimer

-- general function .. we might split this module

function statistics.register(tag,fnc)
    if statistics.enable and type(fnc) == "function" then
        local rt = registered[tag] or (#statusinfo + 1)
        statusinfo[rt] = { tag, fnc }
        registered[tag] = rt
        if #tag > n then n = #tag end
    end
end

local report = logs.reporter("mkiv lua stats")

function statistics.show()
    if statistics.enable then
        -- this code will move
        local register = statistics.register
        register("used platform", function()
            return format("%s, type: %s, binary subtree: %s",
                os.platform or "unknown",os.type or "unknown", environment.texos or "unknown")
        end)
     -- register("luatex banner", function()
     --     return lower(status.banner)
     -- end)
        if LUATEXENGINE == "luametatex" then
            register("used engine", function()
                return format("%s version %s, functionality level %s, format id %s",
                    LUATEXENGINE, LUATEXVERSION, LUATEXFUNCTIONALITY, LUATEXFORMATID)
            end)
        else
            register("used engine", function()
                return format("%s version %s with functionality level %s, banner: %s",
                    LUATEXENGINE, LUATEXVERSION, LUATEXFUNCTIONALITY, lower(status.banner))
            end)
        end
        register("control sequences", function()
            return format("%s of %s + %s", status.cs_count, status.hash_size,status.hash_extra)
        end)
        register("callbacks", statistics.callbacks)
        if TEXENGINE == "luajittex" and JITSUPPORTED then
            local jitstatus = jit.status
            if jitstatus then
                local jitstatus = { jitstatus() }
                if jitstatus[1] then
                    register("luajit options", concat(jitstatus," ",2))
                end
            end
        end
        -- so far
     -- collectgarbage("collect")
        register("lua properties",function()
            local hashchar = tonumber(status.luatex_hashchars)
            local mask = lua.mask or "ascii"
            return format("engine: %s %s, used memory: %s, hash chars: min(%i,40), symbol mask: %s (%s)",
                jit and "luajit" or "lua",
                LUAVERSION,
                statistics.memused(),
                hashchar and 2^hashchar or "unknown",
                mask,
                mask == "utf" and "τεχ" or "tex")
        end)
        register("runtime",statistics.runtime)
        logs.newline() -- initial newline
        for i=1,#statusinfo do
            local s = statusinfo[i]
            local r = s[2]()
            if r then
                report("%s: %s",s[1],r)
            end
        end
     -- logs.newline() -- final newline
        statistics.enable = false
    end
end

function statistics.memused() -- no math.round yet -)
    local round = math.round or math.floor
    return format("%s MB, ctx: %s MB, max: %s MB)",
        round(collectgarbage("count")/1000),
        round(status.luastate_bytes/1000000),
        status.luastate_bytes_max and round(status.luastate_bytes_max/1000000) or "unknown"
    )
end

starttiming(statistics)

function statistics.formatruntime(runtime) -- indirect so it can be overloaded and
    return format("%s seconds", runtime)   -- indeed that happens in cure-uti.lua
end

function statistics.runtime()
    stoptiming(statistics)
 -- stoptiming(statistics) -- somehow we can start the timer twice, but where
    local runtime = lua.getruntime and lua.getruntime() or elapsedtime(statistics)
    return statistics.formatruntime(runtime)
end

local report = logs.reporter("system")

function statistics.timed(action,all)
    starttiming("run")
    action()
    stoptiming("run")
    local runtime = tonumber(elapsedtime("run"))
    if all then
        local alltime = tonumber(lua.getruntime and lua.getruntime() or elapsedtime(statistics))
        if alltime and alltime > 0 then
            report("total runtime: %0.3f seconds of %0.3f seconds",runtime,alltime)
            return
        end
    end
    report("total runtime: %0.3f seconds",runtime)
end

-- goodie

function statistics.tracefunction(base,tag,...)
    for i=1,select("#",...) do
        local name = select(i,...)
        local stat = { }
        local func = base[name]
        setmetatableindex(stat,function(t,k) t[k] = 0 return 0 end)
        base[name] = function(n,k,v) stat[k] = stat[k] + 1 return func(n,k,v) end
        statistics.register(formatters["%s.%s"](tag,name),function() return serialize(stat,"calls") end)
    end
end