Skip to content
This repository has been archived by the owner on Aug 20, 2020. It is now read-only.

[Profnsched] New module (fr) #507

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 77 additions & 0 deletions mods/profnsched/README.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
[profnsched] : Profile and schedule mod (fr)
============================================

Buts :
======
* Objectif principal : maintenir le temps d'exécution du pas (step) au plus près du tick rate (dedicated_server_step)
* analyser en temps réel les temps d'execution de certaines fonctions de module
(actuellement uniquement globalstep et minetest.after)
* Décaler, de façon adaptative, l'exécution des modules qui l'autorisent explicitement en cas de surcharge
* (WIP) Indiquer à chaque module le temps dont il dispose pour s'exécuter (utile pour les modules qui adaptent leurs calculs)


Concepts :
==========
* Les travaux en attentes sont placés dans des files d'executions priorisées
* La première file est toujours exécutée, même en cas de surcharge
* Les autres files sont traitées tant qu'il reste du temps (< tick rate)
* En cas d'arrêt prématuré (surcharge), chaque classe de priorité est transférée dans la précédente (ainsi en cas de surcharge on est certain que le travail sera executé)
* Il peut y avoir autant de files que souhaités (rester raisonnable cependant)
* Concrêtement :
- Dans l'idéal, toutes les files sont exécutées à chaque pas (step) !
- En cas de surcharge, on a la garantie que les travaux de la file n seront exécutés au pas (step) n ou n+1
- Mais on a aucune garantie de temps (à cause de la surcharge, les pas (step) dépassent le tick rate, parfois de plusieurs secondes...)
- Ne pas oublier que de toute façon en cas de surcharge, ni globalstep.after ni minetest.after ne peuvent garantir le temps écoulé
* En cas de surcharge une trace des travaux exécutés est transcrite dans le log du serveur (pour analyse des goulots d'étranglement)


Fonctions utiles :
==================
* Pour programmer :
* scheduler.add(class, job) -- ajoute le travail 'job' dans la file n° 'class' (job = {mod_name, func_id, func_code[, args]})
* scheduler.asap(class, func_code) -- ajoute une fonction à exécuter dans la file n° 'class'
* Pour profiler (commande chat) :
/profnsched (génère un dump de stats au bout du nombre de secondes définies dans minetest.conf)
/profnsched n (génère un dump de stats au bout de n secondes)
/profnsched 0 (dump les stats en temps réel en cas de surcharge)
* Pour profiler (minetest.conf)
profnsched_activate = true/false
profnsched_dump_delay = n (dump des stats au bout de n secondes (si activé))


Comparaisons :
==============
* minetest.globalstep
- Pour les traitements qui *nécessitent* une exécution à chaque pas (step)
- Peut être remplacé par minetest.after(0, *) ou scheduler.asap(0, *) mais cela ajoute du temps de traitement inutile.
* minetest.after
- Pour les traitements qui doivent s'exécuter après un certain temps écoulé
- Dans l'idéal c'est fiable, mais en cas de surcharge le temps peut être largement dépassé (inévitable)
* scheduler.add/asap
- Pour les traitements qui souhaitent être fréquemment exécutés
- mais qui n'ont pas besoin d'une fréquence fixe (tick)
- ni d'une durée précise entre chaque execution
- en bref qui acceptent de sacrifier de leur temps au profit de traitements plus importants qu'eux


Exemples possibles :
====================
* Les calculs capitaux en file 1
* Les traitements d'UI pourraient être en file 2 ou 3
* Les traitements encore moins importants en file supérieure (génération des plantes, ...)


Hack actuel :
=============
* La boucle minetest.after qui vérifie les travaux expirés est classée en faible priorité (file 4)
(dans le meilleur des cas la vérification a lieu à chaque pas (step), sinon on reporte)
* Les travaux eux-mêmes issus de minetest.after sont en file 4
(dans le meilleur des cas ils seront exécutés au prochain pas (step), sinon on reporte)


TODO :
======
* fournir en paramètre au module appelé le temps d'execution souhaité/maximum
* mettre d'autres appels en surveillance (on_step, ...)
* nettoyer/optimiser le code ;)

32 changes: 32 additions & 0 deletions mods/profnsched/after.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
dofile(minetest.get_modpath("profnsched").."/queue.lua")

local jobs = {}
local scheduler = scheduler

-- For minetest.after replacement
local function check_expired_jobs()
local time = core.get_us_time()
for i,job in pairs(jobs) do
if time >= job.expire then
scheduler.add(1, job)
jobs[i] = nil
end
end
scheduler.asap(4, check_expired_jobs)
end
scheduler.asap(4, check_expired_jobs)


-- redefine core.after function
function minetest.after(after, func, ...)
assert(type(func) == "function", "Invalid core.after invocation")
local fname = debug.getinfo(2, "S").linedefined --imprecis
local job = {
func_code = func,
expire = core.get_us_time() + after*1000000,
arg = {...},
mod_name = core.get_last_run_mod(),
func_id = "#"..fname
}
jobs[#jobs+1] = job
end
178 changes: 178 additions & 0 deletions mods/profnsched/init.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
dofile(minetest.get_modpath("profnsched").."/queue.lua")
dofile(minetest.get_modpath("profnsched").."/after.lua")

local durations = {}
local active = minetest.setting_get("profnsched_activate")
local dump_delay = minetest.setting_get("profnsched_dump_delay")
if not active then active = false end
if not dump_delay then dump_delay = 60*5 end


local mathfloor = math.floor

-- Usefull funcs

local function dump_durations(current) --param: current durations or nil (to dump global durations)
if current then
local avg = 0
local dt = 0
for i,v in pairs(current) do
dt = mathfloor(v[3])/1000
avg = mathfloor(durations[v[1]][v[2]].us/durations[v[1]][v[2]].n)/1000
minetest.log("[Profnsched] "..dt.."ms (avg: "..avg.." ; "..durations[v[1]][v[2]].n.." calls) "..v[1].." "..v[2])
end
else
for i,md in pairs(durations) do
for j,fn in pairs(md) do
avg = mathfloor(fn.us/fn.n)/1000
minetest.log("[Profnsched] avg: "..avg.." ; "..fn.n.." calls ; "..i.." "..j)
end
end
end
end

local function activate()
if active then
if dump_delay ~= 0 then
minetest.log("[Profnsched] Will dump global stats in "..dump_delay.."s")
minetest.after(dump_delay, dump_durations)
else
minetest.log("[Profnsched] Now will dump RT stats")
end
end
end
activate()

minetest.register_chatcommand("profnsched", {
params = "<text>",
description = "Activate mods profiling",
func = function(name, param)
active = true
if param == "" then
dump_delay = minetest.setting_get("profnsched_dump_delay")
if not dump_delay then dump_delay = 60*5 end
else
dump_delay = tonumber(param)
end
activate()
end
})




local function update_durations(mod_name, func_id, dtime)
if not durations[mod_name] then
durations[mod_name] = {}
end
if not durations[mod_name][func_id] then
durations[mod_name][func_id] = {
us = 0,
n = 0,
cur = 0
}
end
durations[mod_name][func_id].us = durations[mod_name][func_id].us + dtime
durations[mod_name][func_id].n = durations[mod_name][func_id].n + 1
durations[mod_name][func_id].cur = dtime
end

--------------------------------------------------------------
-- Move olds globalsteps and redefine minetest internal caller

local gs = {} -- global_steps (moved here)

for i,f in ipairs(minetest.registered_globalsteps) do
gs[#gs+1] = {
mod_name = "unknown"..i,
func_id = "unknown(globalstep)",
func_code = f
}
minetest.registered_globalsteps[i] = nil
end

local old_globalstep = minetest.register_globalstep

function minetest.register_globalstep(func)
gs[#gs+1] = {
mod_name = core.get_last_run_mod(),
func_id = "unknown(globalstep)",
func_code = func
}
end



-- Main code

local last_elapsed_local_dtime = 0
local last_internal_server_dtime = 0
local tick_dtime = minetest.setting_get("dedicated_server_step")*1000000

old_globalstep(function(dtime)
local begin_time = core.get_us_time()
last_internal_server_dtime = dtime*1000000 - last_elapsed_local_dtime
local launch_dtime = begin_time - last_internal_server_dtime

local current_durations = {}
current_durations[1] = {"Internal SERVER", "& unprofiled", last_internal_server_dtime}

local tbegin = 0
-- Globalsteps
for i,v in pairs(gs) do
tbegin = core.get_us_time()
v.func_code(dtime+(core.get_us_time()-tbegin)/1000000)
if active then
current_durations[#current_durations+1] = {v.mod_name, v.func_id, core.get_us_time()-tbegin}
end
end

-- Others jobs
local njb = scheduler.waitingjobs()
local jbdone = 0
for class,q in ipairs(scheduler.queue) do
local grp = q.groups[q.first]
for i,job in pairs(grp) do
tbegin = core.get_us_time()
core.set_last_run_mod(job.mod_name)
job.func_code(unpack(job.arg))
jbdone = jbdone+1
current_durations[#current_durations+1] = {job.mod_name, job.func_id, core.get_us_time()-tbegin}
grp[i] = nil
if class > 1 and ((core.get_us_time()-launch_dtime) > tick_dtime) then --class 1 fully processed even on overload
break
end
end
if ((core.get_us_time()-launch_dtime) > tick_dtime) then
break
end
end
scheduler.shift()

local elapsed = (core.get_us_time()-launch_dtime)

-- update all durations
for i,v in pairs(current_durations) do
update_durations(v[1], v[2], v[3])
end

if (elapsed > tick_dtime) then --overload ?
if last_internal_server_dtime < tick_dtime then -- caused by profiled mods ?
if active and dump_delay == 0 then
minetest.log("[Profnsched] Overload ! "..mathfloor(elapsed)/1000 .."ms")
dump_durations(current_durations)
end
else
if active and dump_delay == 0 then
minetest.log("[Profnsched] Overload ! Caused by server or not profiled mods : "..mathfloor(last_internal_server_dtime)/1000 .."ms")
end
end
end

for i,v in pairs(current_durations) do
current_durations[i] = nil
end

last_elapsed_local_dtime = core.get_us_time() - begin_time
end)

83 changes: 83 additions & 0 deletions mods/profnsched/queue.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
scheduler = {}
scheduler.queue = {[1]={first=1, last=2, groups={[1]={}, [2]={}}}}

function scheduler.add(priority, job)
-- get asked class
local class = scheduler.queue[priority]
local p = priority
while not class do -- create all classes under 'priority'
scheduler.queue[p] = {first=1, last=2, groups={[1]={}, [2]={}}}
p = p-1
class = scheduler.queue[p]
end
class = scheduler.queue[priority]
-- get last group
local grp = class.groups[class.last]
-- add job into last group
grp[#grp+1] = job
end

function scheduler.asap(priority, func)
scheduler.add(priority, {
mod_name = core.get_last_run_mod(),
func_id = "#"..debug.getinfo(2, "S").linedefined, --imprecis
func_code = func,
arg = {},
})
end

function scheduler.mdebug(s)
minetest.debug("[Profnsched] "..s)
end

function scheduler.shift()
local nb = scheduler.waitingjobs()
local tsrc = nil
local tdst = nil
for class,q in ipairs(scheduler.queue) do
q.groups[q.first] = q.groups[q.last]
q.groups[q.last] = {}
local tnext = class+1
tnext = scheduler.queue[tnext]
if tnext then
tsrc = tnext.groups[tnext.first]
tdst = q.groups[q.first]
for i,j in pairs(tsrc) do
tdst[#tdst+1] = j
tsrc[i] = nil
end
end
end
if nb ~= scheduler.waitingjobs() then --This should never happen, left because it was used during debug phase
mdebug("ERROR, This should never happen ! Lost jobs, some mod may not work from now, please restart the server.")
end
--
end

function scheduler.fulldebug()
minetest.log("[Profnsched]"..table.getn(scheduler.queue).." classes")
for class,q in pairs(scheduler.queue) do
minetest.log("[Profnsched] class "..class..":")
minetest.log("[Profnsched] "..q.first.." "..q.last.." ("..q.last-q.first+1 .." groups)")
for i,grp in pairs(q.groups) do
local n = 0
for j,jb in pairs(grp) do
n = n+1
end
minetest.log("[Profnsched] group "..i..", "..n.." jobs")
end
end
minetest.log("[Profnsched] end")
end

function scheduler.waitingjobs()
local n = 0
for class, q in pairs(scheduler.queue) do
for i,grp in pairs(q.groups) do
for j,jb in pairs(grp) do
n = n+1
end
end
end
return n
end
1 change: 1 addition & 0 deletions worlds/minetestforfun/world.mt
Original file line number Diff line number Diff line change
Expand Up @@ -226,3 +226,4 @@ load_mod_snow = true

load_mod_profilerdumper = true
load_mod_stacktraceplus = true
load_mod_profnsched = true