aboutsummaryrefslogtreecommitdiff
path: root/builtin/profiler/instrumentation.lua
diff options
context:
space:
mode:
Diffstat (limited to 'builtin/profiler/instrumentation.lua')
-rw-r--r--builtin/profiler/instrumentation.lua232
1 files changed, 232 insertions, 0 deletions
diff --git a/builtin/profiler/instrumentation.lua b/builtin/profiler/instrumentation.lua
new file mode 100644
index 000000000..4311215b2
--- /dev/null
+++ b/builtin/profiler/instrumentation.lua
@@ -0,0 +1,232 @@
+--Minetest
+--Copyright (C) 2016 T4im
+--
+--This program is free software; you can redistribute it and/or modify
+--it under the terms of the GNU Lesser General Public License as published by
+--the Free Software Foundation; either version 2.1 of the License, or
+--(at your option) any later version.
+--
+--This program is distributed in the hope that it will be useful,
+--but WITHOUT ANY WARRANTY; without even the implied warranty of
+--MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+--GNU Lesser General Public License for more details.
+--
+--You should have received a copy of the GNU Lesser General Public License along
+--with this program; if not, write to the Free Software Foundation, Inc.,
+--51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+local format, pairs, type = string.format, pairs, type
+local core, get_current_modname = core, core.get_current_modname
+local profiler, sampler = ...
+local instrument_builtin = core.setting_getbool("instrument.builtin") or false
+
+local register_functions = {
+ register_globalstep = 0,
+ register_playerevent = 0,
+ register_on_placenode = 0,
+ register_on_dignode = 0,
+ register_on_punchnode = 0,
+ register_on_generated = 0,
+ register_on_newplayer = 0,
+ register_on_dieplayer = 0,
+ register_on_respawnplayer = 0,
+ register_on_prejoinplayer = 0,
+ register_on_joinplayer = 0,
+ register_on_leaveplayer = 0,
+ register_on_cheat = 0,
+ register_on_chat_message = 0,
+ register_on_player_receive_fields = 0,
+ register_on_craft = 0,
+ register_craft_predict = 0,
+ register_on_protection_violation = 0,
+ register_on_item_eat = 0,
+ register_on_punchplayer = 0,
+ register_on_player_hpchange = 0,
+}
+
+---
+-- Create an unique instrument name.
+-- Generate a missing label with a running index number.
+--
+local counts = {}
+local function generate_name(def)
+ local class, label, func_name = def.class, def.label, def.func_name
+ if label then
+ if class or func_name then
+ return format("%s '%s' %s", class or "", label, func_name or ""):trim()
+ end
+ return format("%s", label):trim()
+ elseif label == false then
+ return format("%s", class or func_name):trim()
+ end
+
+ local index_id = def.mod .. (class or func_name)
+ local index = counts[index_id] or 1
+ counts[index_id] = index + 1
+ return format("%s[%d] %s", class or func_name, index, class and func_name or ""):trim()
+end
+
+---
+-- Keep `measure` and the closure in `instrument` lean, as these, and their
+-- directly called functions are the overhead that is caused by instrumentation.
+--
+local time, log = core.get_us_time, sampler.log
+local function measure(modname, instrument_name, start, ...)
+ log(modname, instrument_name, time() - start)
+ return ...
+end
+--- Automatically instrument a function to measure and log to the sampler.
+-- def = {
+-- mod = "",
+-- class = "",
+-- func_name = "",
+-- -- if nil, will create a label based on registration order
+-- label = "" | false,
+-- }
+local function instrument(def)
+ if not def or not def.func then
+ return
+ end
+ def.mod = def.mod or get_current_modname()
+ local modname = def.mod
+ local instrument_name = generate_name(def)
+ local func = def.func
+
+ if not instrument_builtin and modname == "*builtin*" then
+ return func
+ end
+
+ return function(...)
+ -- This tail-call allows passing all return values of `func`
+ -- also called https://en.wikipedia.org/wiki/Continuation_passing_style
+ -- Compared to table creation and unpacking it won't lose `nil` returns
+ -- and is expected to be faster
+ -- `measure` will be executed after time() and func(...)
+ return measure(modname, instrument_name, time(), func(...))
+ end
+end
+
+local function can_be_called(func)
+ -- It has to be a function or callable table
+ return type(func) == "function" or
+ ((type(func) == "table" or type(func) == "userdata") and
+ getmetatable(func) and getmetatable(func).__call)
+end
+
+local function assert_can_be_called(func, func_name, level)
+ if not can_be_called(func) then
+ -- Then throw an *helpful* error, by pointing on our caller instead of us.
+ error(format("Invalid argument to %s. Expected function-like type instead of '%s'.", func_name, type(func)), level + 1)
+ end
+end
+
+---
+-- Wraps a registration function `func` in such a way,
+-- that it will automatically instrument any callback function passed as first argument.
+--
+local function instrument_register(func, func_name)
+ local register_name = func_name:gsub("^register_", "", 1)
+ return function(callback, ...)
+ assert_can_be_called(callback, func_name, 2)
+ register_functions[func_name] = register_functions[func_name] + 1
+ return func(instrument {
+ func = callback,
+ func_name = register_name
+ }), ...
+ end
+end
+
+local function init_chatcommand()
+ if core.setting_getbool("instrument.chatcommand") or true then
+ local orig_register_chatcommand = core.register_chatcommand
+ core.register_chatcommand = function(cmd, def)
+ def.func = instrument {
+ func = def.func,
+ label = "/" .. cmd,
+ }
+ orig_register_chatcommand(cmd, def)
+ end
+ end
+end
+
+---
+-- Start instrumenting selected functions
+--
+local function init()
+ local is_set = core.setting_getbool
+ if is_set("instrument.entity") or true then
+ -- Explicitly declare entity api-methods.
+ -- Simple iteration would ignore lookup via __index.
+ local entity_instrumentation = {
+ "on_activate",
+ "on_step",
+ "on_punch",
+ "rightclick",
+ "get_staticdata",
+ }
+ -- Wrap register_entity() to instrument them on registration.
+ local orig_register_entity = core.register_entity
+ core.register_entity = function(name, prototype)
+ local modname = get_current_modname()
+ for _, func_name in pairs(entity_instrumentation) do
+ prototype[func_name] = instrument {
+ func = prototype[func_name],
+ mod = modname,
+ func_name = func_name,
+ label = prototype.label,
+ }
+ end
+ orig_register_entity(name,prototype)
+ end
+ end
+
+ if is_set("instrument.abm") or true then
+ -- Wrap register_abm() to automatically instrument abms.
+ local orig_register_abm = core.register_abm
+ core.register_abm = function(spec)
+ spec.action = instrument {
+ func = spec.action,
+ class = "ABM",
+ label = spec.label,
+ }
+ orig_register_abm(spec)
+ end
+ end
+
+ if is_set("instrument.lbm") or true then
+ -- Wrap register_lbm() to automatically instrument lbms.
+ local orig_register_lbm = core.register_lbm
+ core.register_lbm = function(spec)
+ spec.action = instrument {
+ func = spec.action,
+ class = "LBM",
+ label = spec.label or spec.name,
+ }
+ orig_register_lbm(spec)
+ end
+ end
+
+ if is_set("instrument.global_callback") or true then
+ for func_name, _ in pairs(register_functions) do
+ core[func_name] = instrument_register(core[func_name], func_name)
+ end
+ end
+
+ if is_set("instrument.profiler") or false then
+ -- Measure overhead of instrumentation, but keep it down for functions
+ -- So keep the `return` for better optimization.
+ profiler.empty_instrument = instrument {
+ func = function() return end,
+ mod = "*profiler*",
+ class = "Instrumentation overhead",
+ label = false,
+ }
+ end
+end
+
+return {
+ register_functions = register_functions,
+ instrument = instrument,
+ init = init,
+ init_chatcommand = init_chatcommand,
+}