From 2651262fa3134415f349f63840c89486fabd9063 Mon Sep 17 00:00:00 2001 From: fluxionary <25628292+fluxionary@users.noreply.github.com> Date: Sun, 13 Feb 2022 06:54:41 -0800 Subject: rework mesecons debug to be more flexible (#7) * add proper settings (untested) * more constants -> settings * normalize whitespace between code files * refactor globalsteps in order to simplify logic * minor refactoring * rename file * use mod_storage for persistent data; optimize context initialization * refactoring (moving files around) * rewrite penalty * add settings; document; allow changing while game is running * add command to update settings * update init after splitting commands into files * fix bugs; add debugging tools; too much for one commit... * fix whitelist conversion * add adjustable blinky plant to timer overrides * add some more mesecons nodes with repeating timers * resolve luacheck warnings * tweak hud * Update documentation; parameterize more things; refactor some logic for readability --- .luacheckrc | 2 + README.md | 96 ++++++++++++++---- chatcommands.lua | 131 ------------------------ cleanup.lua | 29 ++++++ clear_penalty.lua | 29 ------ commands/admin_commands.lua | 122 ++++++++++++++++++++++ commands/clear_penalty.lua | 33 ++++++ commands/create_lag.lua | 28 +++++ commands/flush.lua | 8 ++ commands/user_commands.lua | 60 +++++++++++ compat/convert_old_whitelist.lua | 12 +++ context.lua | 45 ++++----- flush.lua | 10 -- functions.lua | 9 -- hud.lua | 151 +++++++++++++++------------ init.lua | 85 +++++++++------- luacontroller.lua | 45 --------- mod.conf | 4 +- nodes/mesecons_lagger.lua | 59 +++++++++++ nodes/penalty_controller.lua | 75 ++++++++++++++ overrides.lua | 36 ------- overrides/mesecons_queue.lua | 49 +++++++++ overrides/node_timers.lua | 69 +++++++++++++ penalty.lua | 213 +++++++++++++++++++++++++++++---------- penalty_controller.lua | 73 -------------- privs.lua | 3 +- settings.lua | 67 ++++++++++++ settingtypes.txt | 51 ++++++++++ util.lua | 21 ++++ whitelist.lua | 21 ---- 30 files changed, 1085 insertions(+), 551 deletions(-) delete mode 100644 chatcommands.lua create mode 100644 cleanup.lua delete mode 100644 clear_penalty.lua create mode 100644 commands/admin_commands.lua create mode 100644 commands/clear_penalty.lua create mode 100644 commands/create_lag.lua create mode 100644 commands/flush.lua create mode 100644 commands/user_commands.lua create mode 100644 compat/convert_old_whitelist.lua delete mode 100644 flush.lua delete mode 100644 functions.lua delete mode 100644 luacontroller.lua create mode 100644 nodes/mesecons_lagger.lua create mode 100644 nodes/penalty_controller.lua delete mode 100644 overrides.lua create mode 100644 overrides/mesecons_queue.lua create mode 100644 overrides/node_timers.lua delete mode 100644 penalty_controller.lua create mode 100644 settings.lua create mode 100644 settingtypes.txt create mode 100644 util.lua delete mode 100644 whitelist.lua diff --git a/.luacheckrc b/.luacheckrc index 50f0eeb..61bc753 100644 --- a/.luacheckrc +++ b/.luacheckrc @@ -1,4 +1,6 @@ +ignore = {"212/_.*"} + globals = { "mesecons_debug", "mesecon", diff --git a/README.md b/README.md index b289345..8e066f5 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,61 @@ # Mesecons Debug Collection -Allows to throttle mesecons activity per mapblock +Throttles mesecons if the server is lagging, in particular when mesecons is causing the lag. # Overview -There is a cpu quota for every mapblock, if that quota is used up -the mesecons contraptions will be slowed down for that mapblock +This mod can penalizes mesecons activity on a per-mapblock basis, which can help laggy or machine-heavy servers +to be less laggy overall. Penalties are implemented as delays on various events. -The current mapblock-stats can be viewed with `/mesecons_hud on` +Lag and per-mapblock mesecons usage are tracked over time, in order to calculate how much to penalize each +mapblock, or whether to reduce the penalty. + +The mod defines 3 regimes of lag, with different scales of penalties. + +* If the server steps are not taking much longer than the time allotted for them, the "low penalty" regime will apply. +* If the server steps are taking too long on average, and mesecons usage isn't particularly high, the "medium penalty" + regime will apply. +* If the server steps are taking too long on average and mesecons usage is high, or the server steps are taking much too + long to execute, the "high penalty" regime will apply. + +Each of these regimes has an associated "scale" and "offset". Every time the penalties are re-evaluated, +they are changed according to this formula: + +```lua + new_penalty = old_penalty + (relative_load * penalty_scale) + penalty_offset +``` + +Here, relative_load is the ratio of how much time the current mapblock spends doing mesecons, to the mean time +(spent doing mesecons) across all mapblocks currently running mesecons. This value is currently clamped between 0.1 +and 10, to prevent certain edge cases from being penalized too rapidly. A value of 10 would mean that the mapblock +under consideration is using 10x as much mesecons as the average mapblock. + +Note that, depending on the values of `penalty_scale` and `penalty_offset`, the new penalty may be *less* than the old +penalty. This is to allow penalties to reach equilibrium under a constant load, and to taper off over time if the +usage in the mapblock declines, or the regime changes. ## Settings -* `mesecons_debug.max_usage_micros` default: 15000 +* `penalty_clear_cooldown = 120` Seconds that a player has to wait between using the `mesecons_clear_penalty` command +* `max_penalty = 120` Upper limit of the per-mapblock penalty +* `penalty_mapblock_disabled = 110` + Completely disable mesecons in a mapblock, if the penalty exceeds this value. + Set above `max_penalty` to disable this feature. +* `penalty_check_steps = 50` # of server steps between updating the penalties +* `gc_interval = 61` Seconds after which data about unloaded mapblocks is removed from memory. +* `hud_refresh_interval = 1` Seconds between updating the client's HUD +* `moderate_lag_ratio = 3` + Ratio between actual and expected length of a server step at which lag is considered "moderate" +* `high_lag_ratio = 9` Ratio between actual and expected length of a server step at which lag is considered "high" +* `high_load_threshold = 0.33` + % of processing a server spends on mesecons at which the mescons load is considered "high". +* `low_penalty_offset = -1` Offset of the penalty in the low-lag regime. +* `low_penalty_scale = 0.1` + Scale of the penalty in the low-lag regime. The default values ensure that nothing is penalized in the low-lag regime. +* `medium_penalty_offset = -0.8` Offset of the penalty in the moderate-lag regime. +* `medium_penalty_scale = 0.2` Scale of the penalty in the moderate-lag regime. +* `high_penalty_offset = -0.5` Offset of the penalty in the high-lag regime. +* `high_penalty_scale = 0.5` Scale of the penalty in the high-lag regime. ## Privs @@ -19,21 +63,39 @@ The current mapblock-stats can be viewed with `/mesecons_hud on` ## Commands +* `/mesecons_clear_penalty` + Clears the penalty for the current mapblock. Users can only execute this every `penalty_clear_cooldown` seconds +* `/mesecons_global_stats` shows the mapblock with the most prominent usage of mesecons activity +* `/mesecons_hud` toggles the hud +* `/mesecons_stats` shows some mesecons stats for the current position + + +### Admin Commands + All of these commands require the `mesecons_debug` privilege. -* `/mesecons_hud [on|off]` enables or disables the hud -* `/mesecons_flush` Flushes the action queue -* `/mesecons_enable` Enable the mesecons queue -* `/mesecons_disable` Disables the mesecons queue -* `/mesecons_stats` shows some mesecons stats for the current position -* `/mesecons_global_stats` shows the mapblock with the most prominent usage of mesecons activity -* `/mesecons_whitelist_get` shows the list of whitelisted mapblocks +* `/create_lag ` + Artificially slow down the server by `microseconds` every `chance` server steps. Useful for debugging this mod. +* `/mesecons_debug_get ` Inspect the current value of a setting. +* `/mesecons_debug_set ` Change a setting value. This does *not* save the value between reboots! +* `/mesecons_disable` Disables mesecons entirely +* `/mesecons_enable` Undoes the above command +* `/mesecons_flush` Flushes the mesecons action queue * `/mesecons_whitelist_add` adds the current mapblock to the whitelist +* `/mesecons_whitelist_get` shows the list of whitelisted mapblocks * `/mesecons_whitelist_remove` removes the current mapblock from the whitelist -## Penalty controller +## Nodes + +### Mesecons Lagger + +A node which can create `n` microseconds of lag once every `chance` server steps. Useful for debugging this mod. + +### Penalty Controller + +Requires the `digiline` mod. -Can query the penalty and usage values of the placed-in mapblock (requires the `digiline` mod) +Can query the penalty and usage values of the mapblock it is placed in. Example code to query it with the luacontroller: @@ -45,9 +107,9 @@ end if event.type == "digiline" and event.channel == "penalty_ctrl" then --[[ event.msg = { - micros = 0, - avg_micros = 0, - penalty = 0, + micros = 0, -- micros_per_second + avg_micros = 0, -- avg_micros_per_second + penalty = 0, -- in seconds whitelisted = false } --]] diff --git a/chatcommands.lua b/chatcommands.lua deleted file mode 100644 index 229de91..0000000 --- a/chatcommands.lua +++ /dev/null @@ -1,131 +0,0 @@ - - -minetest.register_chatcommand("mesecons_hud", { - description = "mesecons_hud on/off", - func = function(name, params) - local enable = params == "on" - mesecons_debug.hud[name] = enable - if enable then - return true, "mesecons hud enabled" - else - return true, "mesecons hud disabled" - end - end -}) - -minetest.register_chatcommand("mesecons_global_stats", { - description = "shows the global mesecons stats", - func = function() - local top_ctx, top_hash - - for hash, ctx in pairs(mesecons_debug.context_store) do - if not top_ctx or top_ctx.avg_micros < ctx.avg_micros then - -- store context with the most average time - top_ctx = ctx - top_hash = hash - end - end - - local txt - if top_ctx then - local pos = minetest.get_position_from_hash(top_hash) - - txt = "Most prominent mesecons usage at mapblock " .. minetest.pos_to_string(pos) .. - " with " .. top_ctx.penalty .. " seconds penalty and " .. top_ctx.avg_micros .. " us average use" - else - txt = "no context available" - end - - return true, txt - end -}) - -minetest.register_chatcommand("mesecons_stats", { - description = "shows some mesecons stats for the current position", - func = function(name) - local player = minetest.get_player_by_name(name) - if not player then - return - end - - local ctx = mesecons_debug.get_context(player:get_pos()) - return true, "Mapblock usage: " .. ctx.avg_micros .. " us/s " .. - "(across " .. mesecons_debug.context_store_size .." mapblocks)" - end -}) - -minetest.register_chatcommand("mesecons_enable", { - description = "enables the mesecons globlastep", - privs = {mesecons_debug=true}, - func = function() - -- flush actions, while we are on it - mesecon.queue.actions = {} - mesecons_debug.enabled = true - return true, "mesecons enabled" - end -}) - -minetest.register_chatcommand("mesecons_disable", { - description = "disables the mesecons globlastep", - privs = {mesecons_debug=true}, - func = function() - mesecons_debug.enabled = false - return true, "mesecons disabled" - end -}) - -minetest.register_chatcommand("mesecons_whitelist_get", { - description = "shows the current mapblock whitelist", - privs = {mesecons_debug=true}, - func = function() - local whitelist = "mesecons whitelist:\n" - local count = 0 - for hash, _ in pairs(mesecons_debug.whitelist) do - whitelist = whitelist .. minetest.pos_to_string(minetest.get_position_from_hash(hash)) .. "\n" - count = count + 1 - end - whitelist = whitelist .. string.format("%d mapblocks whitelisted", count) - - return true, whitelist - end -}) - -minetest.register_chatcommand("mesecons_whitelist_add", { - description = "adds the current mapblock to the whitelist", - privs = {mesecons_debug=true}, - func = function(name) - local player = minetest.get_player_by_name(name) - if not player then - return - end - - local ppos = player:get_pos() - local blockpos = mesecons_debug.get_blockpos(ppos) - local hash = minetest.hash_node_position(blockpos) - - mesecons_debug.whitelist[hash] = true - mesecons_debug.save_whitelist() - - return true, "mapblock whitlisted" - end -}) - -minetest.register_chatcommand("mesecons_whitelist_remove", { - description = "removes the current mapblock from the whitelist", - privs = {mesecons_debug=true}, - func = function(name) - local player = minetest.get_player_by_name(name) - if not player then - return - end - - local ppos = player:get_pos() - local blockpos = mesecons_debug.get_blockpos(ppos) - local hash = minetest.hash_node_position(blockpos) - - mesecons_debug.whitelist[hash] = nil - mesecons_debug.save_whitelist() - - return true, "mapblock removed from whitelist" - end -}) diff --git a/cleanup.lua b/cleanup.lua new file mode 100644 index 0000000..69bd20c --- /dev/null +++ b/cleanup.lua @@ -0,0 +1,29 @@ +local subscribe_for_modification = mesecons_debug.settings._subscribe_for_modification +local gc_interval = mesecons_debug.settings.gc_interval +local cleanup_time_micros = gc_interval * 1000000 + +subscribe_for_modification("gc_interval", function(value) + gc_interval = value + cleanup_time_micros = value * 1000000 +end) + +local context_store = mesecons_debug.context_store + +local cleanup_timer = 0 +minetest.register_globalstep(function(dtime) + cleanup_timer = cleanup_timer + dtime + if cleanup_timer < gc_interval then + return + end + cleanup_timer = 0 + + local now = minetest.get_us_time() + for hash, ctx in pairs(context_store) do + local time_diff = now - ctx.mtime + if time_diff > cleanup_time_micros then + -- remove item + context_store[hash] = nil + mesecons_debug.context_store_size = mesecons_debug.context_store_size - 1 + end + end +end) diff --git a/clear_penalty.lua b/clear_penalty.lua deleted file mode 100644 index 0fe84fe..0000000 --- a/clear_penalty.lua +++ /dev/null @@ -1,29 +0,0 @@ - --- playername => time-of-last-cooldown -local cooldown = {} - -minetest.register_chatcommand("mesecons_clear_penalty", { - description = "clears the penalty in the current mapblock " .. - "(cooldown: " .. mesecons_debug.penalty_clear_cooldown .. ")", - func = function(name) - local player = minetest.get_player_by_name(name) - if not player then - return - end - - local last_cooldown_time = cooldown[name] or 0 - local remaining_time = mesecons_debug.penalty_clear_cooldown - (os.time() - last_cooldown_time) - if remaining_time > 0 then - -- cooldown still in progress - return true, "cooldown still in progress, remaining time: " .. remaining_time .. " seconds" - end - - -- set timer - cooldown[name] = os.time() - - local ctx = mesecons_debug.get_context(player:get_pos()) - ctx.penalty = 0 - - return true, "penalty reset" - end -}) diff --git a/commands/admin_commands.lua b/commands/admin_commands.lua new file mode 100644 index 0000000..19a7cb6 --- /dev/null +++ b/commands/admin_commands.lua @@ -0,0 +1,122 @@ +minetest.register_chatcommand("mesecons_enable", { + description = "enables the mesecons globlastep", + privs = { mesecons_debug = true }, + func = function() + -- flush actions, while we are on it + mesecon.queue.actions = {} + mesecons_debug.mesecons_enabled = true + return true, "mesecons enabled" + end +}) + +minetest.register_chatcommand("mesecons_disable", { + description = "disables the mesecons globlastep", + privs = { mesecons_debug = true }, + func = function() + mesecons_debug.mesecons_enabled = false + return true, "mesecons disabled" + end +}) + +minetest.register_chatcommand("mesecons_whitelist_get", { + description = "shows the current mapblock whitelist", + privs = { mesecons_debug = true }, + func = function() + local count = 0 + local list = {} + for hash, _ in pairs(mesecons_debug.storage:to_table().fields) do + table.insert(list, minetest.pos_to_string(minetest.get_position_from_hash(hash))) + count = count + 1 + end + + return true, ( + "mesecons whitelist:\n" .. + "%s\n" .. + "%i mapblocks whitelisted" + ):format( + table.concat(list, "\n"), + count + ) + end +}) + +minetest.register_chatcommand("mesecons_whitelist_add", { + description = "adds the current mapblock to the whitelist", + privs = { mesecons_debug = true }, + func = function(name) + local player = minetest.get_player_by_name(name) + if not player then + return + end + + local hash = mesecons_debug.hashpos(player:get_pos()) + local ctx = mesecons_debug.get_context(hash) + ctx.whitelisted = true + mesecons_debug.storage:set_string(hash, "1") + + return true, "mapblock whitlisted" + end +}) + +minetest.register_chatcommand("mesecons_whitelist_remove", { + description = "removes the current mapblock from the whitelist", + privs = { mesecons_debug = true }, + func = function(name) + local player = minetest.get_player_by_name(name) + if not player then + return + end + + local hash = mesecons_debug.hashpos(player:get_pos()) + local ctx = mesecons_debug.get_context(hash) + ctx.whitelisted = false + mesecons_debug.storage:set_string(hash, "") + + return true, "mapblock removed from whitelist" + end +}) + +minetest.register_chatcommand("mesecons_debug_set", { + description = "modify mesecons_debug settings", + params = " ", + privs = { mesecons_debug = true }, + func = function(name, params) + local player = minetest.get_player_by_name(name) + if not player or not params then + return false + end + + local setting, value = params:match('^([a-zA-Z0-9_-]+)%s+(.*)$') + value = tonumber(value) + if not setting or not value then + return false + end + + if not mesecons_debug.settings[setting] then + return false, "unknown setting" + end + + mesecons_debug.settings.modify_setting(setting, value) + + return true, "setting updated" + end +}) + +minetest.register_chatcommand("mesecons_debug_get", { + description = "get mesecons_debug settings", + params = "", + privs = { mesecons_debug = true }, + func = function(name, setting) + local player = minetest.get_player_by_name(name) + if not player or not setting then + return false + end + + local value = mesecons_debug.settings[setting] + if value then + return true, tostring(value) + else + return false, "unknown setting" + end + end +}) diff --git a/commands/clear_penalty.lua b/commands/clear_penalty.lua new file mode 100644 index 0000000..cf64103 --- /dev/null +++ b/commands/clear_penalty.lua @@ -0,0 +1,33 @@ +local penalty_clear_cooldown = mesecons_debug.settings.penalty_clear_cooldown + +-- playername => time-of-last-cooldown +local cooldown_expiry_by_name = {} + + +minetest.register_chatcommand("mesecons_clear_penalty", { + description = "clears the penalty in the current mapblock " .. + "(cooldown: " .. penalty_clear_cooldown .. ")", + func = function(name) + local player = minetest.get_player_by_name(name) + if not player then + return + end + + local is_admin = minetest.check_player_privs(player, "mesecons_debug") + if not is_admin then + local now = os.time() + local expires = cooldown_expiry_by_name[name] or 0 + local remaining_time = math.floor(expires - now) + if remaining_time > 0 then + -- cooldown still in progress + return true, "cooldown still in progress, remaining time: " .. remaining_time .. " seconds" + end + cooldown_expiry_by_name[name] = now + penalty_clear_cooldown + end + + local ctx = mesecons_debug.get_context(player:get_pos()) + ctx.penalty = 0 + + return true, "penalty reset" + end +}) diff --git a/commands/create_lag.lua b/commands/create_lag.lua new file mode 100644 index 0000000..6598910 --- /dev/null +++ b/commands/create_lag.lua @@ -0,0 +1,28 @@ +local current_lag = 0 +local lag_chance = 0 + +local wait = mesecons_debug.wait + + +minetest.register_chatcommand("create_lag", { + description = "foce a wait of us for 1 / server steps", + params = " ", + privs = { mesecons_debug = true }, + func = function(_name, setting) + local lag, chance = setting:match('^(%S+)%s+(%S+)$') + lag = tonumber(lag) + chance = tonumber(chance) + if not (lag and chance) then + return false, "can't grok lag duration and chance" + end + current_lag = lag + lag_chance = chance + return true + end, +}) + +minetest.register_globalstep(function(_dtime) + if lag_chance > 0 and current_lag > 0 and math.random() < 1 / lag_chance then + wait(current_lag) + end +end) diff --git a/commands/flush.lua b/commands/flush.lua new file mode 100644 index 0000000..ba3f3cd --- /dev/null +++ b/commands/flush.lua @@ -0,0 +1,8 @@ +minetest.register_chatcommand("mesecons_flush", { + description = "flushes the mesecon actionqueue", + privs = { mesecons_debug = true }, + func = function(name) + minetest.log("warning", "Player " .. name .. " flushes mesecon actionqueue") + mesecon.queue.actions = {} + end +}) diff --git a/commands/user_commands.lua b/commands/user_commands.lua new file mode 100644 index 0000000..5836373 --- /dev/null +++ b/commands/user_commands.lua @@ -0,0 +1,60 @@ +minetest.register_chatcommand("mesecons_hud", { + description = "mesecons_hud toggle", + func = function(name) + local enabled = (not mesecons_debug.hud_enabled_by_playername[name]) or nil + mesecons_debug.hud_enabled_by_playername[name] = enabled + if enabled then + return true, "mesecons hud enabled" + else + return true, "mesecons hud disabled" + end + end +}) + +minetest.register_chatcommand("mesecons_global_stats", { + description = "shows the global mesecons stats", + func = function() + local top_ctx, top_hash + + for hash, ctx in pairs(mesecons_debug.context_store) do + if not top_ctx or top_ctx.avg_micros_per_second < ctx.avg_micros_per_second then + -- store context with the most average time + top_ctx = ctx + top_hash = hash + end + end + + local txt + if top_ctx then + txt = ( + "Most prominent mesecons usage at mapblock %s" .. + " with %f seconds penalty and %i us average use" + ):format( + minetest.pos_to_string(minetest.get_position_from_hash(top_hash)), + top_ctx.penalty, + top_ctx.avg_micros_per_second + ) + else + txt = "no context available" + end + + return true, txt + end +}) + +minetest.register_chatcommand("mesecons_stats", { + description = "shows some mesecons stats for the current position", + func = function(name) + local player = minetest.get_player_by_name(name) + if not player then + return + end + + local ctx = mesecons_debug.get_context(player:get_pos()) + return true, ("Mapblock usage: %i us/s (across %i mapblocks)"):format( + ctx.avg_micros_per_second, + mesecons_debug.context_store_size + ) + end +}) + diff --git a/compat/convert_old_whitelist.lua b/compat/convert_old_whitelist.lua new file mode 100644 index 0000000..c51b667 --- /dev/null +++ b/compat/convert_old_whitelist.lua @@ -0,0 +1,12 @@ +local filename = minetest.get_worldpath() .. "/mesecons_debug_whiltelist" +local file = io.open(filename, "r") + +if file then + local data = file:read("*a") + local whitelist = minetest.deserialize(data) or {} + for hash, _ in pairs(whitelist) do + mesecons_debug.storage:set_string(hash, "1") + end + file:close() + os.remove(filename) +end diff --git a/context.lua b/context.lua index cf9b343..5f22100 100644 --- a/context.lua +++ b/context.lua @@ -1,30 +1,27 @@ +local storage = mesecons_debug.storage -- returns the context data for the node-position mesecons_debug.get_context = function(pos) - local blockpos = mesecons_debug.get_blockpos(pos) - local hash = minetest.hash_node_position(blockpos) + local hash = mesecons_debug.hashpos(pos) + local ctx = mesecons_debug.context_store[hash] - local ctx = mesecons_debug.context_store[hash] - if not ctx then - -- create a new context - ctx = { - -- usage in us - micros = 0, - -- average micros per second - avg_micros = 0, - -- time penalty - penalty = 0, + if not ctx then + -- create a new context + ctx = { + -- usage in us + micros = 0, + -- "running average" micros per second + avg_micros_per_second = 0, + -- time penalty + penalty = 0, + -- modification time + mtime = minetest.get_us_time(), + -- whitelist status + whitelisted = storage:contains(hash) + } + mesecons_debug.context_store[hash] = ctx + mesecons_debug.context_store_size = mesecons_debug.context_store_size + 1 + end - -- mtime - mtime = minetest.get_us_time(), - } - mesecons_debug.context_store[hash] = ctx - end - - -- update context - - -- whitelist flag - ctx.whitelisted = mesecons_debug.whitelist[hash] - - return ctx + return ctx end diff --git a/flush.lua b/flush.lua deleted file mode 100644 index 1f74100..0000000 --- a/flush.lua +++ /dev/null @@ -1,10 +0,0 @@ - - -minetest.register_chatcommand("mesecons_flush", { - description = "flushes the mesecon actionqueue", - privs = {mesecons_debug=true}, - func = function(name) - minetest.log("warning", "Player " .. name .. " flushes mesecon actionqueue") - mesecon.queue.actions = {} - end -}) diff --git a/functions.lua b/functions.lua deleted file mode 100644 index d68396d..0000000 --- a/functions.lua +++ /dev/null @@ -1,9 +0,0 @@ - - -function mesecons_debug.get_blockpos(pos) - return { - x = math.floor(pos.x / 16), - y = math.floor(pos.y / 16), - z = math.floor(pos.z / 16) - } -end diff --git a/hud.lua b/hud.lua index c769d37..d8a4e25 100644 --- a/hud.lua +++ b/hud.lua @@ -1,84 +1,103 @@ -local HUD_POSITION = {x = 0.1, y = 0.8} -local HUD_ALIGNMENT = {x = 1, y = 0} +local hud_refresh_interval = mesecons_debug.settings.hud_refresh_interval +mesecons_debug.settings._subscribe_for_modification("hud_refresh_interval", + function(value) hud_refresh_interval = value end) -local hud = {} - - -minetest.register_on_joinplayer(function(player) - local hud_data = {} - hud[player:get_player_name()] = hud_data - - hud_data.txt = player:hud_add({ - hud_elem_type = "text", - position = HUD_POSITION, - offset = {x = 0, y = 0}, - text = "", - alignment = HUD_ALIGNMENT, - scale = {x = 100, y = 100}, - number = 0xFF0000 - }) - -end) +local HUD_POSITION = { x = 0.1, y = 0.8 } +local HUD_ALIGNMENT = { x = 1, y = 0 } +local hudid_by_playername = {} minetest.register_on_leaveplayer(function(player) - hud[player:get_player_name()] = nil + hudid_by_playername[player:get_player_name()] = nil end) - - local function get_info(player) - local pos = player:get_pos() - local blockpos = mesecons_debug.get_blockpos(pos) - local ctx = mesecons_debug.get_context(pos) + local pos = player:get_pos() + local blockpos = mesecons_debug.get_blockpos(pos) + local ctx = mesecons_debug.get_context(pos) - local percent = math.floor(ctx.avg_micros / mesecons_debug.max_usage_micros * 100) + local total = mesecons_debug.avg_total_micros_per_second + if total == 0 then total = 1 end + local percent = ctx.avg_micros_per_second * 100 / total - local txt = "Mesecons @ (" .. blockpos.x .. "/" .. blockpos.y .. "/" .. blockpos.z .. ") " + local txt = ("mesecons @ %s\n"):format( + minetest.pos_to_string(blockpos) + ) - if ctx.whitelisted then - txt = txt .. "whitelisted, no limits" - return txt, 0x00FF00 - end - - txt = txt .. - " usage: " .. ctx.avg_micros .. " us/s .. (" .. percent .. "%) " .. - "penalty: " .. math.floor(ctx.penalty*10)/10 .. " s" + if ctx.whitelisted then + txt = txt .. "whitelisted, no limits" + return txt, 0x00FFFF + end - if ctx.penalty <= 0.1 then - return txt, 0x00FF00 - elseif ctx.penalty < 0.5 then - return txt, 0xFFFF00 - else - return txt, 0xFF0000 - end + txt = txt .. ("usage: %.0f us/s .. (%.1f%%) penalty: %.2fs"):format( + ctx.avg_micros_per_second, + percent, + ctx.penalty + ) + txt = txt .. ("\nlag: %.2f (%s); mesecons load = %s"):format( + mesecons_debug.avg_lag, + mesecons_debug.lag_level, + mesecons_debug.load_level + ) + if minetest.get_server_max_lag then + txt = txt .. ("; max_lag: %.2f"):format( + minetest.get_server_max_lag() + ) + end + txt = txt .. ("; #players = %i"):format( + #minetest.get_connected_players() + ) + txt = txt .. ("\npenalties enabled = %s; mesecons enabled = %s"):format( + mesecons_debug.enabled, + mesecons_debug.mesecons_enabled + ) + + if ctx.penalty <= 1 then + return txt, 0x00FF00 + elseif ctx.penalty <= 10 then + return txt, 0xFFFF00 + else + return txt, 0xFF0000 + end end local timer = 0 minetest.register_globalstep(function(dtime) - timer = timer + dtime - if timer < 1 then - return - end - timer = 0 - - for _, player in ipairs(minetest.get_connected_players()) do - local playername = player:get_player_name() - local hud_data = hud[playername] - local hud_enable = mesecons_debug.hud[playername] - if hud_enable then - local txt, color = get_info(player) - player:hud_change(hud_data.txt, "text", txt) - player:hud_change(hud_data.txt, "color", color) - - elseif hud_enable == false then - mesecons_debug.hud[playername] = nil - player:hud_change(hud_data.txt, "text", "") - + timer = timer + dtime + if timer < hud_refresh_interval then + return + end + timer = 0 + + for _, player in ipairs(minetest.get_connected_players()) do + local playername = player:get_player_name() + local hudid = hudid_by_playername[playername] + local hud_enabled = mesecons_debug.hud_enabled_by_playername[playername] + + if hud_enabled then + local text, color = get_info(player) + if hudid then + player:hud_change(hudid, "text", text) + player:hud_change(hudid, "number", color) + + else + hudid_by_playername[playername] = player:hud_add({ + hud_elem_type = "text", + position = HUD_POSITION, + offset = { x = 0, y = 0 }, + text = text, + number = color, + alignment = HUD_ALIGNMENT, + scale = { x = 100, y = 100 }, + }) + end + + else + if hudid then + player:hud_remove(hudid) + hudid_by_playername[playername] = nil + end + end end - - end - - end) diff --git a/init.lua b/init.lua index 84bc862..b9b5c1f 100644 --- a/init.lua +++ b/init.lua @@ -1,46 +1,63 @@ -local MP = minetest.get_modpath("mesecons_debug") +local modname = minetest.get_current_modname() +local modpath = minetest.get_modpath(modname) mesecons_debug = { - enabled = true, - -- blockpos-hash => context - context_store = {}, - context_store_size = 0, + -- is mesecons_debug enabled? + enabled = true, - -- max penalty in seconds - max_penalty = 300, + -- is mescons enabled? + mesecons_enabled = true, - -- everything above this threshold will disable the mesecons in that mapblock - penalty_mapblock_disabled = 60, + -- blockpos-hash => context + context_store = {}, + context_store_size = 0, - -- time between /mesecons_clear_penalty commands, in seconds - penalty_clear_cooldown = 120, + -- persistent storage for whitelist + storage = minetest.get_mod_storage(), - -- mapblock-hash -> true - whitelist = {}, + -- total amount of time used by mesecons in the last period + total_micros = 0, - -- playername => true - hud = {}, + -- running average of how much mesecons is doing + avg_total_micros_per_second = 0, - -- cpu usage in microseconds that triggers the penalty mechanism - max_usage_micros = tonumber(minetest.settings:get("mesecons_debug.max_usage_micros")) or 15000 + -- average lag + avg_lag = 1, + lag_level = 'none', + load_level = 'none', + + -- playername => true + hud_enabled_by_playername = {}, + + -- which optional dependencies are installed? + has = { + monitoring = minetest.get_modpath("monitoring"), + digilines = minetest.get_modpath("digilines"), + }, + + log = function(level, message_fmt, ...) + minetest.log(level, ("[%s] "):format(modname) .. message_fmt:format(...)) + end } -dofile(MP.."/functions.lua") -dofile(MP.."/whitelist.lua") -dofile(MP.."/privs.lua") -dofile(MP.."/flush.lua") -dofile(MP.."/context.lua") -dofile(MP.."/penalty.lua") -dofile(MP.."/clear_penalty.lua") -dofile(MP.."/overrides.lua") -dofile(MP.."/luacontroller.lua") -dofile(MP.."/chatcommands.lua") -dofile(MP.."/hud.lua") - -if minetest.get_modpath("digilines") then - dofile(MP.."/penalty_controller.lua") +dofile(modpath .. "/settings.lua") +dofile(modpath .. "/util.lua") +dofile(modpath .. "/privs.lua") +dofile(modpath .. "/context.lua") +dofile(modpath .. "/penalty.lua") +dofile(modpath .. "/cleanup.lua") +dofile(modpath .. "/hud.lua") +dofile(modpath .. "/overrides/mesecons_queue.lua") +dofile(modpath .. "/overrides/node_timers.lua") +dofile(modpath .. "/commands/user_commands.lua") +dofile(modpath .. "/commands/admin_commands.lua") +dofile(modpath .. "/commands/create_lag.lua") +dofile(modpath .. "/commands/clear_penalty.lua") +dofile(modpath .. "/commands/flush.lua") + +dofile(modpath .. "/nodes/mesecons_lagger.lua") +if mesecons_debug.has.digilines then + dofile(modpath .. "/nodes/penalty_controller.lua") end -mesecons_debug.load_whitelist() - -print("[OK] mesecons_debug loaded") +dofile(modpath .. "/compat/convert_old_whitelist.lua") diff --git a/luacontroller.lua b/luacontroller.lua deleted file mode 100644 index c143bdc..0000000 --- a/luacontroller.lua +++ /dev/null @@ -1,45 +0,0 @@ - -local function override_node_timer(node_name) - local def = minetest.registered_nodes[node_name] - local old_node_timer = def.on_timer - def.on_timer = function(pos) - local ctx = mesecons_debug.get_context(pos) - if ctx.penalty > 0 then - -- defer - local timer = minetest.get_node_timer(pos) - local meta = minetest.get_meta(pos) - local is_defered = meta:get_int("_defered") == 1 - - if is_defered then - -- already delayed - meta:set_int("_defered", 0) - return old_node_timer(pos) - else - -- start timer - meta:set_int("_defered", 1) - timer:start(ctx.penalty) - end - else - -- immediate - return old_node_timer(pos) - end - end -end - --- luaC -local BASENAME = "mesecons_luacontroller:luacontroller" -for a = 0, 1 do -- 0 = off 1 = on - for b = 0, 1 do - for c = 0, 1 do - for d = 0, 1 do - local cid = tostring(d)..tostring(c)..tostring(b)..tostring(a) - local node_name = BASENAME..cid - override_node_timer(node_name) - end - end - end -end - --- blinky -override_node_timer("mesecons_blinkyplant:blinky_plant_off") -override_node_timer("mesecons_blinkyplant:blinky_plant_on") diff --git a/mod.conf b/mod.conf index 4693667..b3bbbc0 100644 --- a/mod.conf +++ b/mod.conf @@ -1,3 +1,3 @@ name = mesecons_debug -depends = mesecons, mesecons_blinkyplant, mesecons_luacontroller -optional_depends = monitoring, digilines +depends = mesecons +optional_depends = digilines, digistuff, mesecons_blinkyplant, mesecons_luacontroller, moremesecons_adjustable_blinkyplant, moremesecons_injector_controller, monitoring, pipeworks diff --git a/nodes/mesecons_lagger.lua b/nodes/mesecons_lagger.lua new file mode 100644 index 0000000..e4d966e --- /dev/null +++ b/nodes/mesecons_lagger.lua @@ -0,0 +1,59 @@ + +local wait = mesecons_debug.wait + +mesecon.queue:add_function("create_lag", function(_pos, duration) + wait(duration) +end) + + +minetest.register_node("mesecons_debug:mesecons_lagger", { + description = "machine for adding artificial mesecons lag", + group = { + not_in_creative_inventory = 1, + unbreakable = 1, + }, + tiles = {"default_mese_block.png^[colorize:#F00:128"}, + on_blast = function() end, + drop = "", + + on_construct = function(pos) + local meta = minetest.get_meta(pos) + meta:set_float("lag", 0.0) + meta:set_float("chance", 0.0) + meta:set_string("formspec", + ("field[lag;Lag (in us);%s]field[chance;Chance;%s]"):format(0.0, 0.0)) + + local timer = minetest.get_node_timer(pos) + timer:start(0) + end, + + on_receive_fields = function(pos, _formname, fields, sender) + if not minetest.check_player_privs(sender, "mesecons_debug") then + return + end + local meta = minetest.get_meta(pos) + if fields.lag then + meta:set_float("lag", fields.lag) + end + if fields.chance then + meta:set_float("chance", fields.chance) + end + meta:set_string("formspec", + ("field[lag;Lag (in us);%s]field[chance;Chance;%s]"):format( + meta:get_float("lag"), meta:get_float("chance"))) + + end, + + on_timer = function(pos, _elapsed) + local meta = minetest.get_meta(pos) + local lag = meta:get_float("lag") + local chance = meta:get_float("chance") + if lag > 0 and chance > 0 then + if math.random() < 1 / chance then + mesecon.queue:add_action(pos, "create_lag", { lag }) + end + end + + return true + end, +}) diff --git a/nodes/penalty_controller.lua b/nodes/penalty_controller.lua new file mode 100644 index 0000000..6e160c1 --- /dev/null +++ b/nodes/penalty_controller.lua @@ -0,0 +1,75 @@ +minetest.register_node("mesecons_debug:penalty_controller", { + description = "Mesecons penalty controller", + groups = { + cracky = 3, + not_in_creative_inventory = 1, + }, + + on_construct = function(pos) + local meta = minetest.get_meta(pos) + meta:set_string("formspec", "field[channel;Channel;${channel}") + end, + + tiles = { + "penalty_controller_top.png", + "jeija_microcontroller_bottom.png", + "jeija_microcontroller_sides.png", + "jeija_microcontroller_sides.png", + "jeija_microcontroller_sides.png", + "jeija_microcontroller_sides.png" + }, + + inventory_image = "penalty_controller_top.png", + drawtype = "nodebox", + selection_box = { + --From luacontroller + type = "fixed", + fixed = { -8 / 16, -8 / 16, -8 / 16, 8 / 16, -5 / 16, 8 / 16 }, + }, + node_box = { + --From Luacontroller + type = "fixed", + fixed = { + { -8 / 16, -8 / 16, -8 / 16, 8 / 16, -7 / 16, 8 / 16 }, -- Bottom slab + { -5 / 16, -7 / 16, -5 / 16, 5 / 16, -6 / 16, 5 / 16 }, -- Circuit board + { -3 / 16, -6 / 16, -3 / 16, 3 / 16, -5 / 16, 3 / 16 }, -- IC + } + }, + + paramtype = "light", + sunlight_propagates = true, + + on_receive_fields = function(pos, _, fields, sender) + local name = sender:get_player_name() + if minetest.is_protected(pos, name) and not minetest.check_player_privs(name, { protection_bypass = true }) then + minetest.record_protection_violation(pos, name) + return + end + local meta = minetest.get_meta(pos) + if fields.channel then + meta:set_string("channel", fields.channel) + end + end, + + digiline = { + receptor = {}, + effector = { + action = function(pos, _, channel, msg) + local meta = minetest.get_meta(pos) + if meta:get_string("channel") ~= channel then + return + end + if msg == "GET" then + local ctx = mesecons_debug.get_context(pos) + -- copy and send values + digiline:receptor_send(pos, digiline.rules.default, channel, { + micros = ctx.micros, + avg_micros = ctx.avg_micros_per_second, + penalty = ctx.penalty, + whitelisted = ctx.whitelisted + }) + end + end + } + } +}) diff --git a/overrides.lua b/overrides.lua deleted file mode 100644 index b50b6e5..0000000 --- a/overrides.lua +++ /dev/null @@ -1,36 +0,0 @@ - --- execute() -local old_execute = mesecon.queue.execute -mesecon.queue.execute = function(self, action) - if mesecons_debug.enabled then - local t0 = minetest.get_us_time() - old_execute(self, action) - local t1 = minetest.get_us_time() - local micros = t1 - t0 - - local ctx = mesecons_debug.get_context(action.pos) - ctx.micros = ctx.micros + micros - ctx.mtime = t0 - - --print("execute() func=" .. action.func .. " pos=" .. minetest.pos_to_string(action.pos) .. " micros=" .. micros) - end -end - - --- add_action() -local old_add_action = mesecon.queue.add_action -mesecon.queue.add_action = function(self, pos, func, params, time, overwritecheck, priority) - if mesecons_debug.enabled then - local ctx = mesecons_debug.get_context(pos) - - time = time or 0 - time = time + ctx.penalty - if time > mesecons_debug.penalty_mapblock_disabled then - -- penalty exceeded disable-threshold, don't even add the action - return - end - - old_add_action(self, pos, func, params, time, overwritecheck, priority) - --print("add_action() pos=" .. minetest.pos_to_string(pos)) - end -end diff --git a/overrides/mesecons_queue.lua b/overrides/mesecons_queue.lua new file mode 100644 index 0000000..4375d7d --- /dev/null +++ b/overrides/mesecons_queue.lua @@ -0,0 +1,49 @@ +local penalty_mapblock_disabled = mesecons_debug.settings.penalty_mapblock_disabled +mesecons_debug.settings._subscribe_for_modification("penalty_mapblock_disabled", + function(value) penalty_mapblock_disabled = value end) + +-- execute() +local old_execute = mesecon.queue.execute +mesecon.queue.execute = function(self, action) + if not mesecons_debug.enabled then + return old_execute(self, action) + elseif not mesecons_debug.mesecons_enabled then + return + end + + local ctx = mesecons_debug.get_context(action.pos) + if ctx.whitelisted then + return old_execute(self, action) + end + + local t0 = minetest.get_us_time() + local rv = old_execute(self, action) + local micros = minetest.get_us_time() - t0 + + mesecons_debug.total_micros = mesecons_debug.total_micros + micros + ctx.micros = ctx.micros + micros + ctx.mtime = t0 -- modification time + + return rv +end + + +-- add_action() +local old_add_action = mesecon.queue.add_action +mesecon.queue.add_action = function(self, pos, func, params, time, overwritecheck, priority) + if not mesecons_debug.enabled then + return old_add_action(self, pos, func, params, time, overwritecheck, priority) + + elseif not mesecons_debug.mesecons_enabled then + return + end + + local ctx = mesecons_debug.get_context(pos) + + if not ctx.whitelisted and ctx.penalty > penalty_mapblock_disabled then + -- penalty exceeded disable-threshold, don't even add the action + return + end + + return old_add_action(self, pos, func, params, (time or 0) + ctx.penalty, overwritecheck, priority) +end diff --git a/overrides/node_timers.lua b/overrides/node_timers.lua new file mode 100644 index 0000000..ea37d6a --- /dev/null +++ b/overrides/node_timers.lua @@ -0,0 +1,69 @@ +local function override_node_timer(node_name) + local old_node_timer = minetest.registered_nodes[node_name].on_timer + minetest.override_item(node_name, { + on_timer = function(pos, elapsed) + if not mesecons_debug.enabled then + return old_node_timer(pos, elapsed) + + elseif not mesecons_debug.mesecons_enabled then + return true + end + + local ctx = mesecons_debug.get_context(pos) + + if ctx.whitelisted or elapsed > ctx.penalty then + return old_node_timer(pos, elapsed) + else + -- defer + return true + end + end, + }) +end + +if minetest.get_modpath("digistuff") then + override_node_timer("digistuff:timer") +end + +if minetest.get_modpath("mesecons_luacontroller") then + for a = 0, 1 do + for b = 0, 1 do + for c = 0, 1 do + for d = 0, 1 do + override_node_timer(("mesecons_luacontroller:luacontroller%i%i%i%i"):format(a, b, c, d)) + end + end + end + end +end + +if minetest.get_modpath("mesecons_blinkyplant") then + override_node_timer("mesecons_blinkyplant:blinky_plant_off") + override_node_timer("mesecons_blinkyplant:blinky_plant_on") +end + +if minetest.get_modpath("moremesecons_adjustable_blinkyplant") then + override_node_timer("moremesecons_adjustable_blinkyplant:adjustable_blinky_plant_off") + override_node_timer("moremesecons_adjustable_blinkyplant:adjustable_blinky_plant_on") +end + +if minetest.get_modpath("moremesecons_injector_controller") then + override_node_timer("moremesecons_injector_controller:injector_controller_on") + override_node_timer("moremesecons_injector_controller:injector_controller_off") +end + +if minetest.get_modpath("pipeworks") then + for a = 0, 1 do + for b = 0, 1 do + for c = 0, 1 do + for d = 0, 1 do + for e = 0, 1 do + for f = 0, 1 do + override_node_timer(("pipeworks:lua_tube%i%i%i%i%i%i"):format(a, b, c, d, e, f)) + end + end + end + end + end + end +end diff --git a/penalty.lua b/penalty.lua index b7173b7..1e20706 100644 --- a/penalty.lua +++ b/penalty.lua @@ -1,60 +1,169 @@ -local has_monitoring = minetest.get_modpath("monitoring") +local expected_dtime = tonumber(minetest.settings:get("dedicated_server_step")) or 0.09 -local mapblock_count, penalized_mapblock_count +local subscribe_for_modification = mesecons_debug.settings._subscribe_for_modification +local max_penalty = mesecons_debug.settings.max_penalty +subscribe_for_modification("max_penalty", function(value) max_penalty = value end) +local moderate_lag_ratio = mesecons_debug.settings.moderate_lag_ratio +subscribe_for_modification("moderate_lag_ratio", function(value) moderate_lag_ratio = value end) +local high_lag_ratio = mesecons_debug.settings.high_lag_ratio +local high_lag_dtime = expected_dtime * high_lag_ratio +subscribe_for_modification("high_lag_ratio", function(value) + high_lag_ratio = value + high_lag_dtime = expected_dtime * value +end) +local high_load_threshold = mesecons_debug.settings.high_load_threshold +subscribe_for_modification("high_load_threshold", function(value) high_load_threshold = value end) +local penalty_check_steps = mesecons_debug.settings.penalty_check_steps +subscribe_for_modification("penalty_check_steps", function(value) penalty_check_steps = value end) +local high_penalty_scale = mesecons_debug.settings.high_penalty_scale +subscribe_for_modification("high_penalty_scale", function(value) high_penalty_scale = value end) +local high_penalty_offset = mesecons_debug.settings.high_penalty_offset +subscribe_for_modification("high_penalty_offset", function(value) high_penalty_offset = value end) +local medium_penalty_scale = mesecons_debug.settings.medium_penalty_scale +subscribe_for_modification("medium_penalty_scale", function(value) medium_penalty_scale = value end) +local medium_penalty_offset = mesecons_debug.settings.medium_penalty_offset +subscribe_for_modification("medium_penalty_offset", function(value) medium_penalty_offset = value end) +local low_penalty_scale = mesecons_debug.settings.low_penalty_scale +subscribe_for_modification("low_penalty_scale", function(value) low_penalty_scale = value end) +local low_penalty_offset = mesecons_debug.settings.low_penalty_offset +subscribe_for_modification("low_penalty_offset", function(value) low_penalty_offset = value end) +local relative_load_max = mesecons_debug.settings.relative_load_clamp +local relative_load_min = 1 / mesecons_debug.settings.relative_load_clamp +subscribe_for_modification("relative_load_clamp", function(value) + relative_load_max = value + relative_load_min = 1 / value +end) +-- see https://en.wikipedia.org/w/index.php?title=Moving_average&oldid=1069105690#Exponential_moving_average +local averaging_coefficient = mesecons_debug.settings.averaging_coefficient +subscribe_for_modification("averaging_coefficient", function(value) averaging_coefficient = value end) + +local max = math.max +local min = math.min + +local function clamp(low, value, high) + return max(low, min(value, high)) +end + +local function clamp_load(load) + return max(relative_load_min, min(load, relative_load_max)) +end +local function update_average(current, history) + return (current * averaging_coefficient) + (history * (1 - averaging_coefficient)) +end + +local has_monitoring = mesecons_debug.has.monitoring +local mapblock_count, penalized_mapblock_count if has_monitoring then - mapblock_count = monitoring.gauge("mesecons_debug_mapblock_count", "count of tracked mapblocks") - penalized_mapblock_count = monitoring.gauge("mesecons_debug_penalized_mapblock_count", "count of penalized mapblocks") + mapblock_count = monitoring.gauge("mesecons_debug_mapblock_count", "count of tracked mapblocks") + penalized_mapblock_count = monitoring.gauge("mesecons_debug_penalized_mapblock_count", + "count of penalized mapblocks") end -local timer = 0 +local elapsed_steps = 0 +local elapsed = 0 + minetest.register_globalstep(function(dtime) - timer = timer + dtime - if timer < 1 then return end - timer=0 - - local penalized_count = 0 - local now = minetest.get_us_time() - local cleanup_time_micros = 300 * 1000 * 1000 - - mesecons_debug.context_store_size = 0 - for hash, ctx in pairs(mesecons_debug.context_store) do - local time_diff = now - ctx.mtime - if time_diff > cleanup_time_micros then - -- remove item - mesecons_debug.context_store[hash] = nil - - else - -- calculate moving average - ctx.avg_micros = math.floor((ctx.avg_micros * 0.8) + (ctx.micros * 0.2)) - -- reset cpu usage counter - ctx.micros = 0 - - -- apply penalty values - if ctx.avg_micros > (mesecons_debug.max_usage_micros * 10) then - -- 10 times the limit used, potential abuse, add a greater penalty value - ctx.penalty = math.min(ctx.penalty + 5, mesecons_debug.max_penalty) - - elseif ctx.avg_micros > mesecons_debug.max_usage_micros then - -- add penalty value - ctx.penalty = math.min(ctx.penalty + 0.2, mesecons_debug.max_penalty) - - elseif ctx.penalty > 0 then - -- remove penalty (very slowly) - ctx.penalty = math.max(ctx.penalty - 0.001, 0) - end - - mesecons_debug.context_store_size = mesecons_debug.context_store_size + 1 - if ctx.penalty > 0 then - penalized_count = penalized_count + 1 - end - - end - end - - if has_monitoring then - mapblock_count.set(mesecons_debug.context_store_size) - penalized_mapblock_count.set(penalized_count) - end + elapsed = elapsed + dtime + elapsed_steps = elapsed_steps + 1 + + --[[ + we check every N steps instead of every T seconds because we are more interested in the length of the steps + than in the number of them. + we also force a check if a particular step takes quite a long time, to keep things responsive. + ]] + if dtime < high_lag_dtime and elapsed_steps < penalty_check_steps then + return + end + + local context_store_size = mesecons_debug.context_store_size -- # of blocks w/ active mesecons + local total_micros = mesecons_debug.total_micros + local total_micros_per_second = total_micros / elapsed + local avg_total_micros_per_second = update_average(total_micros_per_second, + mesecons_debug.avg_total_micros_per_second) + mesecons_debug.avg_total_micros_per_second = avg_total_micros_per_second + + if context_store_size == 0 or avg_total_micros_per_second == 0 then + -- nothing to do, but reset counters + elapsed = 0 + elapsed_steps = 0 + mesecons_debug.total_micros = 0 + return + end + + + -- how much lag is there? + local lag = elapsed / (elapsed_steps * expected_dtime) + local avg_lag = update_average(lag, mesecons_debug.avg_lag) + mesecons_debug.avg_lag = avg_lag + + local is_high_lag = avg_lag > high_lag_ratio + local is_moderate_lag = avg_lag > moderate_lag_ratio + + -- how much of the lag was mesecons? + local mesecons_load = avg_total_micros_per_second / 1000000 + local is_high_load = mesecons_load > high_load_threshold + + -- for use by HUD + if is_high_lag then + mesecons_debug.lag_level = 'high' + elseif is_moderate_lag then + mesecons_debug.lag_level = 'moderate' + else + mesecons_debug.lag_level = 'low' + end + + -- for use by HUD + if is_high_load then + mesecons_debug.load_level = 'high' + else + mesecons_debug.load_level = 'low' + end + + local penalty_scale, penalty_offset + if is_high_lag or (is_moderate_lag and is_high_load) then + penalty_scale = high_penalty_scale + penalty_offset = high_penalty_offset + elseif is_moderate_lag then + penalty_scale = medium_penalty_scale + penalty_offset = medium_penalty_offset + else + penalty_scale = low_penalty_scale + penalty_offset = low_penalty_offset + end + + -- avg load per active context + local avg_avg_micros_per_second = avg_total_micros_per_second / context_store_size + + local penalized_count = 0 -- for monitoring + for _, ctx in pairs(mesecons_debug.context_store) do + if not ctx.whitelisted then + -- moving avg + local micros_per_second = ctx.micros / elapsed + local avg_micros_per_second = update_average(micros_per_second, ctx.avg_micros_per_second) + ctx.avg_micros_per_second = avg_micros_per_second + + local relative_load = clamp_load(avg_micros_per_second / avg_avg_micros_per_second) + + local new_penalty = ctx.penalty + (relative_load * penalty_scale) + penalty_offset + ctx.penalty = clamp(0, new_penalty, max_penalty) + + if has_monitoring and new_penalty > 0 then + penalized_count = penalized_count + 1 + end + + -- reset cpu usage counter + ctx.micros = 0 + end + end + + if has_monitoring then + mapblock_count.set(mesecons_debug.context_store_size) + penalized_mapblock_count.set(penalized_count) + end + -- reset counters + elapsed = 0 + elapsed_steps = 0 + mesecons_debug.total_micros = 0 end) diff --git a/penalty_controller.lua b/penalty_controller.lua deleted file mode 100644 index 33093ca..0000000 --- a/penalty_controller.lua +++ /dev/null @@ -1,73 +0,0 @@ - -minetest.register_node("mesecons_debug:penalty_controller", { - description = "Mesecons penalty controller", - groups = { - cracky=3 - }, - - on_construct = function(pos) - local meta = minetest.get_meta(pos) - meta:set_string("formspec","field[channel;Channel;${channel}") - end, - - tiles = { - "penalty_controller_top.png", - "jeija_microcontroller_bottom.png", - "jeija_microcontroller_sides.png", - "jeija_microcontroller_sides.png", - "jeija_microcontroller_sides.png", - "jeija_microcontroller_sides.png" - }, - - inventory_image = "penalty_controller_top.png", - drawtype = "nodebox", - selection_box = { - --From luacontroller - type = "fixed", - fixed = { -8/16, -8/16, -8/16, 8/16, -5/16, 8/16 }, - }, - node_box = { - --From Luacontroller - type = "fixed", - fixed = { - {-8/16, -8/16, -8/16, 8/16, -7/16, 8/16}, -- Bottom slab - {-5/16, -7/16, -5/16, 5/16, -6/16, 5/16}, -- Circuit board - {-3/16, -6/16, -3/16, 3/16, -5/16, 3/16}, -- IC - } - }, - - paramtype = "light", - sunlight_propagates = true, - - on_receive_fields = function(pos, _, fields, sender) - local name = sender:get_player_name() - if minetest.is_protected(pos,name) and not minetest.check_player_privs(name,{protection_bypass=true}) then - minetest.record_protection_violation(pos,name) - return - end - local meta = minetest.get_meta(pos) - if fields.channel then meta:set_string("channel",fields.channel) end - end, - - digiline = { - receptor = {}, - effector = { - action = function(pos, _, channel, msg) - local meta = minetest.get_meta(pos) - if meta:get_string("channel") ~= channel then - return - end - if msg == "GET" then - local ctx = mesecons_debug.get_context(pos) - -- copy and send values - digiline:receptor_send(pos, digiline.rules.default, channel, { - micros = ctx.micros, - avg_micros = ctx.avg_micros, - penalty = ctx.penalty, - whitelisted = ctx.whitelisted - }) - end - end - } - } -}) diff --git a/privs.lua b/privs.lua index eededf9..c7d1075 100644 --- a/privs.lua +++ b/privs.lua @@ -1,4 +1,3 @@ - minetest.register_privilege("mesecons_debug", { - description = "Allows execution of mesecon debug chatcommands" + description = "Allows execution of mesecon debug chatcommands" }) diff --git a/settings.lua b/settings.lua new file mode 100644 index 0000000..fa1254a --- /dev/null +++ b/settings.lua @@ -0,0 +1,67 @@ +mesecons_debug.settings = { + -- in seconds + hud_refresh_interval = tonumber(minetest.settings:get("mesecons_debug.hud_refresh_interval")) or 1, + + -- max penalty in seconds + max_penalty = tonumber(minetest.settings:get("mesecons_debug.max_penalty")) or 120, + + -- everything above this threshold will disable the mesecons in that mapblock entirely + penalty_mapblock_disabled = tonumber(minetest.settings:get("mesecons_debug.penalty_mapblock_disabled")) or 110, + + -- time between /mesecons_clear_penalty commands, in seconds + penalty_clear_cooldown = tonumber(minetest.settings:get("mesecons_debug.penalty_clear_cooldown")) or 120, + + -- remove unmodified penalty data for a mapblock from memory after this many seconds + gc_interval = tonumber(minetest.settings:get("mesecons_debug.gc_interval")) or 61, + + -- steps between updating penalties + penalty_check_steps = tonumber(minetest.settings:get("mesecons_debug.penalty_check_steps")) or 50, + + -- ratio of actual to expected duration of server steps + moderate_lag_ratio = tonumber(minetest.settings:get("mesecons_debug.moderate_lag_ratio")) or 3, + + -- ratio of actual to expected duration of server steps + high_lag_ratio = tonumber(minetest.settings:get("mesecons_debug.high_lag_ratio")) or 10, + + -- percentage of total server load due to mesecons + high_load_threshold = tonumber(minetest.settings:get("mesecons_debug.high_load_threshold")) or 0.33, + + -- scale of penalty during high load + high_penalty_scale = tonumber(minetest.settings:get("mesecons_debug.high_penalty_scale")) or 0.5, + + -- offset of penalty during high load + high_penalty_offset = tonumber(minetest.settings:get("mesecons_debug.high_penalty_offset")) or -0.5, + + -- scale of penalty during medium load + medium_penalty_scale = tonumber(minetest.settings:get("mesecons_debug.medium_penalty_scale")) or 0.2, + + -- offset of penalty during medium load + medium_penalty_offset = tonumber(minetest.settings:get("mesecons_debug.medium_penalty_offset")) or -0.67, + + -- scale of penalty during low load + low_penalty_scale = tonumber(minetest.settings:get("mesecons_debug.low_penalty_scale")) or 0.1, + + -- offset of penalty during low load + low_penalty_offset = tonumber(minetest.settings:get("mesecons_debug.low_penalty_offset")) or -1, + + -- forces (1 / clamp) <= relative load <= clamp + relative_load_clamp = tonumber(minetest.settings:get("mesecons_debug.relative_load_clamp")) or 10, + + -- coefficient used in calculating an exponential moving average of values across penalty checks + averaging_coefficient = tonumber(minetest.settings:get("mesecons_debug.averaging_coefficient")) or 0.2, + + _listeners = {}, + _subscribe_for_modification = function(name, func) + local listeners = mesecons_debug.settings._listeners[name] or {} + table.insert(listeners, func) + mesecons_debug.settings._listeners[name] = listeners + end, + + modify_setting = function(name, value) + value = tonumber(value) + mesecons_debug.settings[name] = value + for _, func in ipairs(mesecons_debug.settings._listeners[name] or {}) do + func(value) + end + end, +} diff --git a/settingtypes.txt b/settingtypes.txt new file mode 100644 index 0000000..23cbbf5 --- /dev/null +++ b/settingtypes.txt @@ -0,0 +1,51 @@ +# in seconds +mesecons_debug.hud_refresh_interval (hud refresh interval) int 1 1 60 + +# max penalty in seconds +mesecons_debug.max_penalty (maximum penalty) int 120 1 3600 + +# everything above this threshold will disable the mesecons in that mapblock +mesecons_debug.penalty_mapblock_disabled (threshold to disable mapblock) int 110 1 3600 + +# time between /mesecons_clear_penalty commands, in seconds +mesecons_debug.penalty_clear_cooldown (penalty clear command cooldown interval) int 120 1 3600 + +# remove unused mapblock penalty data from memory after this many seconds +mesecons_debug.gc_interval (garbage collection interval) float 61 1 3600 + +# ratio between actual and expected duration of server steps, above which is considered laggy +mesecons_debug.moderate_lag_ratio (low lag ratio) float 3 1 1000 + +# ratio between actual and expected duration of server steps, above which is considered very laggy +mesecons_debug.high_lag_ratio (high lag ratio) float 9 1 1000 + +# percent of server step that is due solely to mesecons, above which is considered excessive +mesecons_debug.high_load_threshold (high load threshold) float 0.33 0 1 + +# number of server steps between penalty updates +mesecons_debug.penalty_check_steps (steps between penalty updates) float 50 1 10000 + +# scale of penalty during high load +mesecons_debug.high_penalty_scale (high penalty scale) float 0.5 0.01 1 + +# offset of penalty during high load +mesecons_debug.high_penalty_offset (high penalty offset) float -0.5 -1 0 + +# scale of penalty during medium load +mesecons_debug.medium_penalty_scale (medium penalty scale) float 0.2 0.01 1 + +# offset of penalty during medium load +mesecons_debug.medium_penalty_offset (medium penalty offset) float -0.8 -1 0 + +# scale of penalty during low load +mesecons_debug.low_penalty_scale (low penalty scale) float 0.1 0.01 1 + +# offset of penalty during low load +mesecons_debug.low_penalty_offset (low penalty offset) float -1 -1 0 + +# forces (1 / clamp) <= relative load <= clamp +mesecons_debug.relative_load_clamp (clamp of relative load value) float 10 1 100 + +# coefficient used in calculating an exponential moving average of values across penalty checks. +# smaller values give more weight to history, larger values give more weight to the present. +mesecons_debug.averaging_coefficient (averaging coefficient) float 0.2 0.01 1 diff --git a/util.lua b/util.lua new file mode 100644 index 0000000..1e45914 --- /dev/null +++ b/util.lua @@ -0,0 +1,21 @@ +function mesecons_debug.get_blockpos(pos) + return { + x = math.floor(pos.x / 16), + y = math.floor(pos.y / 16), + z = math.floor(pos.z / 16) + } +end + +function mesecons_debug.hashpos(pos) + return minetest.hash_node_position({ + x = math.floor(pos.x / 16), + y = math.floor(pos.y / 16), + z = math.floor(pos.z / 16) + }) +end + + +function mesecons_debug.wait(n) + local wait_until = minetest.get_us_time() + n + while minetest.get_us_time() < wait_until do end +end diff --git a/whitelist.lua b/whitelist.lua deleted file mode 100644 index 3b713f0..0000000 --- a/whitelist.lua +++ /dev/null @@ -1,21 +0,0 @@ -local filename = minetest.get_worldpath() .. "/mesecons_debug_whiltelist" - -function mesecons_debug.save_whitelist() - local file = io.open(filename,"w") - local data = minetest.serialize(mesecons_debug.whitelist) - if file and file:write(data) and file:close() then - return - else - minetest.log("error","mesecons_debug: save failed") - return - end -end - -function mesecons_debug.load_whitelist() - local file = io.open(filename, "r") - - if file then - local data = file:read("*a") - mesecons_debug.whitelist = minetest.deserialize(data) or {} - end -end -- cgit v1.2.3