Aaron Suen c8b680b5fc Explain lagometer in built-in help text
Got tired of people not being able to figure out how
to read it and having to explain it each time.
2021-03-10 09:16:09 -05:00

210 lines
7.0 KiB
Lua

-- LUALOCALS < ---------------------------------------------------------
local ipairs, loadstring, math, minetest, pairs, string, tonumber
= ipairs, loadstring, math, minetest, pairs, string, tonumber
local math_ceil, math_floor, string_format, string_gsub, string_rep,
string_sub
= math.ceil, math.floor, string.format, string.gsub, string.rep,
string.sub
-- LUALOCALS > ---------------------------------------------------------
local modname = minetest.get_current_modname()
------------------------------------------------------------------------
-- SETTINGS
local function getconf(suff)
return tonumber(minetest.settings:get(modname .. "_" .. suff))
end
-- How often to publish updates to players. Too infrequent and the meter
-- is no longer as "real-time", but too frequent and they'll get
-- bombarded with HUD change packets.
local interval = getconf("interval") or 2
-- The amount of time in each measurement period. This is also the
-- amount of time between each period expiration.
local period_length = getconf("period_length") or 2
-- The number of time periods across which to accumualate statistics.
local period_count = getconf("period_count") or 31
-- Size of buckets into which dtime values are sorted in weighted
-- histogram.
local bucket_step = getconf("bucket_step") or 0.05
-- Maximum number of buckets. All step times too large for any other
-- bucket will go into the highest bucket.
local bucket_max = getconf("bucket_max") or 20
-- Maximum number of characters to use for ascii bar graph
local graphbar_width = getconf("graphbar_width") or 40
-- Constructor function to pre-initialize a new period table.
local newperiod = loadstring("return {" .. string_rep("0,", bucket_max) .. "}")
------------------------------------------------------------------------
-- MEASUREMENT
-- Queue of accounting periods.
local periods = {}
-- Precise game runtime clock.
local clock = 0
-- Collect statistics at each step.
minetest.register_globalstep(function(dtime)
-- Update clock.
clock = clock + dtime
-- Find current accounting period, initialize
-- if not already present.
local key = math_floor(clock / period_length)
local cur = periods[key]
if not cur then
cur = newperiod()
periods[key] = cur
end
-- Find correct histogram bucket.
local bucket = math_floor(dtime / bucket_step)
if bucket > bucket_max then bucket = bucket_max
elseif bucket < 1 then bucket = 1 end
-- Add weight to bucket.
cur[bucket] = cur[bucket] + dtime
end)
------------------------------------------------------------------------
-- USER TOGGLE
-- Create a separate privilege for players to see the lagometer. This
-- feature is too "internal" to show to all players unconditionally,
-- but not so "internal" that it should depend on the "server" priv.
minetest.register_privilege("lagometer", "Can see the lagometer")
local helptext = string_format(string_gsub([[
The lagometer is a weighted histogram of the probability distribution of
server step times over a sliding window of the past ~%d seconds. The
vertical axis is the step time, with labels on the right indicating the
upper bound of each bucket. The horizontal axis is the total amount of
time that was spent doing steps of that size, with value labels along
the left side. Each server step will add its size to the largest bucket
that fits it. The largest bucket (%0.2f) also includes all lag spikes
too large to fit in any bucket. Old step time is removed from each bucket
once it is older than the sliding window size.]], "%s+", " "),
period_length * (period_count - 1),
bucket_max * bucket_step)
-- Command to manually toggle the lagometer.
minetest.register_chatcommand("lagometer", {
description = "Toggle the lagometer\n\n" .. helptext,
privs = {lagometer = true},
func = function(name)
local player = minetest.get_player_by_name(name)
if not player then return end
local old = player:get_meta():get_string("lagometer") or ""
local v = (old == "") and "1" or ""
player:get_meta():set_string("lagometer", v)
minetest.chat_send_player(name, "Lagometer: "
.. (v ~= "" and "ON" or "OFF"))
end,
})
------------------------------------------------------------------------
-- REPORTING
-- Keep track of connected players and their meters.
local meters = {}
-- Pre-allocated bar graph.
local graphbar = string_rep("|", graphbar_width)
-- Function to publish current lag values to all receiving parties.
local function publish()
-- Expire old periods, and accumulate current ones.
local accum = newperiod()
do
local curkey = math_floor(clock / period_length)
for pk, pv in pairs(periods) do
if pk <= curkey - period_count then
periods[pk] = nil
else
for ik, iv in ipairs(pv) do
accum[ik] = accum[ik] + iv
end
end
end
end
-- Construct the weighted historgram visualization.
for bucket = 1, bucket_max do
local qty = accum[bucket]
local line = qty <= 0 and "" or string_format(" % 2.2f % s % 2.2f % s", qty,
-- Maximum width of a graph bar corresponds to 50% of the total
-- time in the window, so that there will never be 2 bars of the
-- same length that don't have the same amount of time, even if
-- there is one bar that's longer than all others and is cut off.
string_sub(graphbar, 1, math_ceil(qty * 2 * graphbar_width
/ period_length / period_count)),
bucket * bucket_step,
string_rep("\n", bucket - 1))
-- Apply the appropriate text to each meter.
for _, player in ipairs(minetest.get_connected_players()) do
local pname = player:get_player_name()
local meter = meters[pname]
if not meter then
meter = {}
meters[pname] = meter
end
local mline = meter[bucket]
-- Players with privilege will see the meter, players without
-- will get an empty string. The meters are always left in place
-- rather than added/removed for simplicity, and to make it easier
-- to handle when the priv is granted/revoked while the player
-- is connected.
local text = ""
if minetest.get_player_privs(pname).lagometer
and (player:get_meta():get_string("lagometer") or "") ~= ""
then text = line end
-- Only apply the text if it's changed, to minimize the risk of
-- generating useless unnecessary packets.
if text ~= "" and not mline then
meter[bucket] = {
text = text,
hud = player:hud_add({
hud_elem_type = "text",
position = {x = 1, y = 1},
text = text,
alignment = {x = -1, y = -1},
number = 0xC0C0C0,
offset = {x = -4, y = -4}
})
}
elseif mline and text == "" then
player:hud_remove(mline.hud)
meter[bucket] = nil
elseif mline and mline.text ~= text then
player:hud_change(mline.hud, "text", text)
mline.text = text
end
end
end
end
-- Run the publish method on a timer, so that player displays
-- are updated while lag is falling off.
local function update()
publish()
minetest.after(interval, update)
end
minetest.after(0, update)
-- Remove meter registrations when players leave.
minetest.register_on_leaveplayer(function(player)
meters[player:get_player_name()] = nil
end)