minetest/builtin/profiler/sampling.lua

207 lines
5.8 KiB
Lua

--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 setmetatable = setmetatable
local pairs, format = pairs, string.format
local min, max, huge = math.min, math.max, math.huge
local core = core
local profiler = ...
-- Split sampler and profile up, to possibly allow for rotation later.
local sampler = {}
local profile
local stats_total
local logged_time, logged_data
local _stat_mt = {
get_time_avg = function(self)
return self.time_all/self.samples
end,
get_part_avg = function(self)
if not self.part_all then
return 100 -- Extra handling for "total"
end
return self.part_all/self.samples
end,
}
_stat_mt.__index = _stat_mt
function sampler.reset()
-- Accumulated logged time since last sample.
-- This helps determining, the relative time a mod used up.
logged_time = 0
-- The measurements taken through instrumentation since last sample.
logged_data = {}
profile = {
-- Current mod statistics (max/min over the entire mod lifespan)
-- Mod specific instrumentation statistics are nested within.
stats = {},
-- Current stats over all mods.
stats_total = setmetatable({
samples = 0,
time_min = huge,
time_max = 0,
time_all = 0,
part_min = 100,
part_max = 100
}, _stat_mt)
}
stats_total = profile.stats_total
-- Provide access to the most recent profile.
sampler.profile = profile
end
---
-- Log a measurement for the sampler to pick up later.
-- Keep `log` and its often called functions lean.
-- It will directly add to the instrumentation overhead.
--
function sampler.log(modname, instrument_name, time_diff)
if time_diff <= 0 then
if time_diff < 0 then
-- This **might** have happened on a semi-regular basis with huge mods,
-- resulting in negative statistics (perhaps midnight time jumps or ntp corrections?).
core.log("warning", format(
"Time travel of %s::%s by %dµs.",
modname, instrument_name, time_diff
))
end
-- Throwing these away is better, than having them mess with the overall result.
return
end
local mod_data = logged_data[modname]
if mod_data == nil then
mod_data = {}
logged_data[modname] = mod_data
end
mod_data[instrument_name] = (mod_data[instrument_name] or 0) + time_diff
-- Update logged time since last sample.
logged_time = logged_time + time_diff
end
---
-- Return a requested statistic.
-- Initialize if necessary.
--
local function get_statistic(stats_table, name)
local statistic = stats_table[name]
if statistic == nil then
statistic = setmetatable({
samples = 0,
time_min = huge,
time_max = 0,
time_all = 0,
part_min = 100,
part_max = 0,
part_all = 0,
}, _stat_mt)
stats_table[name] = statistic
end
return statistic
end
---
-- Update a statistic table
--
local function update_statistic(stats_table, time)
stats_table.samples = stats_table.samples + 1
-- Update absolute time (µs) spend by the subject
stats_table.time_min = min(stats_table.time_min, time)
stats_table.time_max = max(stats_table.time_max, time)
stats_table.time_all = stats_table.time_all + time
-- Update relative time (%) of this sample spend by the subject
local current_part = (time/logged_time) * 100
stats_table.part_min = min(stats_table.part_min, current_part)
stats_table.part_max = max(stats_table.part_max, current_part)
stats_table.part_all = stats_table.part_all + current_part
end
---
-- Sample all logged measurements each server step.
-- Like any globalstep function, this should not be too heavy,
-- but does not add to the instrumentation overhead.
--
local function sample(dtime)
-- Rare, but happens and is currently of no informational value.
if logged_time == 0 then
return
end
for modname, instruments in pairs(logged_data) do
local mod_stats = get_statistic(profile.stats, modname)
if mod_stats.instruments == nil then
-- Current statistics for each instrumentation component
mod_stats.instruments = {}
end
local mod_time = 0
for instrument_name, time in pairs(instruments) do
if time > 0 then
mod_time = mod_time + time
local instrument_stats = get_statistic(mod_stats.instruments, instrument_name)
-- Update time of this sample spend by the instrumented function.
update_statistic(instrument_stats, time)
-- Reset logged data for the next sample.
instruments[instrument_name] = 0
end
end
-- Update time of this sample spend by this mod.
update_statistic(mod_stats, mod_time)
end
-- Update the total time spend over all mods.
stats_total.time_min = min(stats_total.time_min, logged_time)
stats_total.time_max = max(stats_total.time_max, logged_time)
stats_total.time_all = stats_total.time_all + logged_time
stats_total.samples = stats_total.samples + 1
logged_time = 0
end
---
-- Setup empty profile and register the sampling function
--
function sampler.init()
sampler.reset()
if core.settings:get_bool("instrument.profiler") then
core.register_globalstep(function()
if logged_time == 0 then
return
end
return profiler.empty_instrument()
end)
core.register_globalstep(profiler.instrument {
func = sample,
mod = "*profiler*",
class = "Sampler (update stats)",
label = false,
})
else
core.register_globalstep(sample)
end
end
return sampler