-- luacheck: globals debuggery whosit minetest futil -- based on debuggery by flux, but outputs result in chat and calculates max/avg local f = string.format local S = debuggery.S local get_us_time = minetest.get_us_time local log = debuggery.log local log_level = minetest.settings:get("debug_log_level") or "action" local pairs_by_key = futil.table.pairs_by_key local s = debuggery.settings whosit = _G.whosit or {} _G.whosit = whosit local old_values = whosit.instrument_old_values or {} whosit.instrument_old_values = old_values local total_steps = whosit.instrument_total_steps or {} whosit.instrument_total_steps = total_steps local total_elapsed = whosit.instrument_total_elapsed or {} whosit.instrument_total_elapsed = total_elapsed local total_calls = whosit.instrument_total_calls or {} whosit.instrument_total_calls = total_calls local max_time = whosit.instrument_max_time or {} whosit.instrument_max_time = max_time local num_instrumented = 0 local function instrument(name, value, _cache) if type(value) == "function" then log("action", "instrumenting %q", name) return function(...) if s.instrument_log_every_call then log("action", "%s(%s)", name, dump({ ... })) end local begin = get_us_time() local rvs = { value(...) } local elapsed = get_us_time() - begin total_elapsed[name] = (total_elapsed[name] or 0) + elapsed total_calls[name] = (total_calls[name] or 0) + 1 max_time[name] = math.max(elapsed, max_time[name] or 0) -- if s.instrument_log_every_call then -- log("action", "%s(...) -> %s", name, dump(rvs)) -- end return unpack(rvs) end elseif type(value) == "table" then _cache = _cache or {} local cached = _cache[value] if cached then return cached end local t = {} _cache[value] = t for k, v in pairs(value) do if type(k) == "string" then t[k] = instrument(f("%s.%s", name, k), v, _cache) else t[k] = instrument(f("%s[%s]", name, k), v, _cache) end end setmetatable(t, instrument(f("getmetatable(%s)", name), getmetatable(value), _cache)) return t else return value end end local function instrument_mod(mod) log("action", "instrumenting %s", mod) old_values[mod] = _G[mod] _G[mod] = instrument(mod, _G[mod]) num_instrumented = num_instrumented + 1 end local function uninstrument_mod(mod) log("action", "uninstrumenting %s", mod) _G[mod] = old_values[mod] old_values[mod] = nil num_instrumented = num_instrumented - 1 end minetest.register_chatcommand( "whosit_instrument_mod", { params = S(""), description = S("toggles recording timing data for all functions declared in a particular global"), privs = { [s.admin_priv] = true }, func = function(name, param) if param == "" then local mods = {} for mod in pairs(old_values) do table.insert(mods, mod) end if #mods == 0 then return true, S("no mods currently instrumented") else return true, S("mods currently instrumented: @1", table.concat(mods, ", ")) end end if not (minetest.global_exists(param) and _G[param]) then return false, S("unknown global @1", param) end if old_values[param] then uninstrument_mod(param) return true, S("instrumentation disabled for @1", param) else instrument_mod(param) return true, S("instrumentation enabled for @1", param) end end, }) local function get_modname(fname) local dot = fname:find('.',1,true) if dot then return fname:sub(1, dot - 1) else return fname end end function whosit.instrument_summary_lines(perstep) if futil.table.is_empty(total_calls) then return false, "nothing to report" end local out = {} for name, num_calls in pairs_by_key(total_calls, function(a,b) return total_elapsed[a] > total_elapsed[b] end) do local steps = perstep and total_steps[get_modname(name)] or 1 local te = math.round(total_elapsed[name]) local avg = total_elapsed[name]/num_calls local tm = max_time[name] table.insert(out, f("%15.1f | n:%11d | avg:%9.1f | max:%9.1f -- %s()", te/steps, num_calls/steps, avg, tm, name)) end return true, out end minetest.register_chatcommand( "whosit_instrument_summary", { -- params = S(""), -- description = S("toggles recording timing data for all functions declared in a particular global"), privs = { [s.admin_priv] = true }, func = function(name, param) if futil.table.is_empty(total_calls) then return false, "nothing to report" end local ok, res = whosit.instrument_summary_lines() if ok then core.chat_send_player(name, table.concat(res, '\n')) end return true, "done." end, }) minetest.register_chatcommand( "whosit_instrument_reset", { -- params = S(""), -- description = S("toggles recording timing data for all functions declared in a particular global"), privs = { [s.admin_priv] = true }, func = function(name, param) for name, _ in pairs(total_calls) do total_calls[name] = nil total_elapsed[name] = nil max_time[name] = nil end for name, _ in pairs(total_steps) do total_steps[name] = nil end return true, "Stats reset." end, }) function whosit.instrument_summary_text(perstep) local _,out = whosit.instrument_summary_lines(perstep) return table.concat(out, '\n') end function whosit.instrument_update_steps() for name,_ in pairs(old_values) do total_steps[name] = (total_steps[name] or 0) + 1 end end if not whosit.instrument_globalstep_registered then minetest.register_globalstep( function(_dtime) whosit.instrument_update_steps() end ) whosit.instrument_globalstep_registered = true end