278 lines
7.4 KiB
Lua
278 lines
7.4 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 3.0 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 DIR_DELIM, LINE_DELIM = DIR_DELIM, "\n"
|
|
local table, unpack, string, pairs, io, os = table, unpack, string, pairs, io, os
|
|
local rep, sprintf, tonumber = string.rep, string.format, tonumber
|
|
local core, settings = core, core.settings
|
|
local reporter = {}
|
|
|
|
---
|
|
-- Shorten a string. End on an ellipsis if shortened.
|
|
--
|
|
local function shorten(str, length)
|
|
if str and str:len() > length then
|
|
return "..." .. str:sub(-(length-3))
|
|
end
|
|
return str
|
|
end
|
|
|
|
local function filter_matches(filter, text)
|
|
return not filter or string.match(text, filter)
|
|
end
|
|
|
|
local function format_number(number, fmt)
|
|
number = tonumber(number)
|
|
if not number then
|
|
return "N/A"
|
|
end
|
|
return sprintf(fmt or "%d", number)
|
|
end
|
|
|
|
local Formatter = {
|
|
new = function(self, object)
|
|
object = object or {}
|
|
object.out = {} -- output buffer
|
|
self.__index = self
|
|
return setmetatable(object, self)
|
|
end,
|
|
__tostring = function (self)
|
|
return table.concat(self.out, LINE_DELIM)
|
|
end,
|
|
print = function(self, text, ...)
|
|
if (...) then
|
|
text = sprintf(text, ...)
|
|
end
|
|
|
|
if text then
|
|
-- Avoid format unicode issues.
|
|
text = text:gsub("Ms", "µs")
|
|
end
|
|
|
|
table.insert(self.out, text or LINE_DELIM)
|
|
end,
|
|
flush = function(self)
|
|
table.insert(self.out, LINE_DELIM)
|
|
local text = table.concat(self.out, LINE_DELIM)
|
|
self.out = {}
|
|
return text
|
|
end
|
|
}
|
|
|
|
local widths = { 50, 8, 8, 8, 5, 5, 5 }
|
|
local txt_row_format = sprintf(" %%-%ds | %%%ds | %%%ds | %%%ds | %%%ds | %%%ds | %%%ds", unpack(widths))
|
|
|
|
local HR = {}
|
|
for i=1, #widths do
|
|
HR[i]= rep("-", widths[i])
|
|
end
|
|
-- ' | ' should break less with github than '-+-', when people are pasting there
|
|
HR = sprintf("-%s-", table.concat(HR, " | "))
|
|
|
|
local TxtFormatter = Formatter:new {
|
|
format_row = function(self, modname, instrument_name, statistics)
|
|
local label
|
|
if instrument_name then
|
|
label = shorten(instrument_name, widths[1] - 5)
|
|
label = sprintf(" - %s %s", label, rep(".", widths[1] - 5 - label:len()))
|
|
else -- Print mod_stats
|
|
label = shorten(modname, widths[1] - 2) .. ":"
|
|
end
|
|
|
|
self:print(txt_row_format, label,
|
|
format_number(statistics.time_min),
|
|
format_number(statistics.time_max),
|
|
format_number(statistics:get_time_avg()),
|
|
format_number(statistics.part_min, "%.1f"),
|
|
format_number(statistics.part_max, "%.1f"),
|
|
format_number(statistics:get_part_avg(), "%.1f")
|
|
)
|
|
end,
|
|
format = function(self, filter)
|
|
local profile = self.profile
|
|
self:print("Values below show absolute/relative times spend per server step by the instrumented function.")
|
|
self:print("A total of %d samples were taken", profile.stats_total.samples)
|
|
|
|
if filter then
|
|
self:print("The output is limited to '%s'", filter)
|
|
end
|
|
|
|
self:print()
|
|
self:print(
|
|
txt_row_format,
|
|
"instrumentation", "min Ms", "max Ms", "avg Ms", "min %", "max %", "avg %"
|
|
)
|
|
self:print(HR)
|
|
for modname,mod_stats in pairs(profile.stats) do
|
|
if filter_matches(filter, modname) then
|
|
self:format_row(modname, nil, mod_stats)
|
|
|
|
if mod_stats.instruments ~= nil then
|
|
for instrument_name, instrument_stats in pairs(mod_stats.instruments) do
|
|
self:format_row(nil, instrument_name, instrument_stats)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
self:print(HR)
|
|
if not filter then
|
|
self:format_row("total", nil, profile.stats_total)
|
|
end
|
|
end
|
|
}
|
|
|
|
local CsvFormatter = Formatter:new {
|
|
format_row = function(self, modname, instrument_name, statistics)
|
|
self:print(
|
|
"%q,%q,%d,%d,%d,%d,%d,%f,%f,%f",
|
|
modname, instrument_name,
|
|
statistics.samples,
|
|
statistics.time_min,
|
|
statistics.time_max,
|
|
statistics:get_time_avg(),
|
|
statistics.time_all,
|
|
statistics.part_min,
|
|
statistics.part_max,
|
|
statistics:get_part_avg()
|
|
)
|
|
end,
|
|
format = function(self, filter)
|
|
self:print(
|
|
"%q,%q,%q,%q,%q,%q,%q,%q,%q,%q",
|
|
"modname", "instrumentation",
|
|
"samples",
|
|
"time min µs",
|
|
"time max µs",
|
|
"time avg µs",
|
|
"time all µs",
|
|
"part min %",
|
|
"part max %",
|
|
"part avg %"
|
|
)
|
|
for modname, mod_stats in pairs(self.profile.stats) do
|
|
if filter_matches(filter, modname) then
|
|
self:format_row(modname, "*", mod_stats)
|
|
|
|
if mod_stats.instruments ~= nil then
|
|
for instrument_name, instrument_stats in pairs(mod_stats.instruments) do
|
|
self:format_row(modname, instrument_name, instrument_stats)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
}
|
|
|
|
local function format_statistics(profile, format, filter)
|
|
local formatter
|
|
if format == "csv" then
|
|
formatter = CsvFormatter:new {
|
|
profile = profile
|
|
}
|
|
else
|
|
formatter = TxtFormatter:new {
|
|
profile = profile
|
|
}
|
|
end
|
|
formatter:format(filter)
|
|
return formatter:flush()
|
|
end
|
|
|
|
---
|
|
-- Format the profile ready for display and
|
|
-- @return string to be printed to the console
|
|
--
|
|
function reporter.print(profile, filter)
|
|
if filter == "" then filter = nil end
|
|
return format_statistics(profile, "txt", filter)
|
|
end
|
|
|
|
---
|
|
-- Serialize the profile data and
|
|
-- @return serialized data to be saved to a file
|
|
--
|
|
local function serialize_profile(profile, format, filter)
|
|
if format == "lua" or format == "json" or format == "json_pretty" then
|
|
local stats = filter and {} or profile.stats
|
|
if filter then
|
|
for modname, mod_stats in pairs(profile.stats) do
|
|
if filter_matches(filter, modname) then
|
|
stats[modname] = mod_stats
|
|
end
|
|
end
|
|
end
|
|
if format == "lua" then
|
|
return core.serialize(stats)
|
|
elseif format == "json" then
|
|
return core.write_json(stats)
|
|
elseif format == "json_pretty" then
|
|
return core.write_json(stats, true)
|
|
end
|
|
end
|
|
-- Fall back to textual formats.
|
|
return format_statistics(profile, format, filter)
|
|
end
|
|
|
|
local worldpath = core.get_worldpath()
|
|
local function get_save_path(format, filter)
|
|
local report_path = settings:get("profiler.report_path") or ""
|
|
if report_path ~= "" then
|
|
core.mkdir(sprintf("%s%s%s", worldpath, DIR_DELIM, report_path))
|
|
end
|
|
return (sprintf(
|
|
"%s/%s/profile-%s%s.%s",
|
|
worldpath,
|
|
report_path,
|
|
os.date("%Y%m%dT%H%M%S"),
|
|
filter and ("-" .. filter) or "",
|
|
format
|
|
):gsub("[/\\]+", DIR_DELIM))-- Clean up delims
|
|
end
|
|
|
|
---
|
|
-- Save the profile to the world path.
|
|
-- @return success, log message
|
|
--
|
|
function reporter.save(profile, format, filter)
|
|
if not format or format == "" then
|
|
format = settings:get("profiler.default_report_format") or "txt"
|
|
end
|
|
if filter == "" then
|
|
filter = nil
|
|
end
|
|
|
|
local path = get_save_path(format, filter)
|
|
|
|
local output, io_err = io.open(path, "w")
|
|
if not output then
|
|
return false, "Saving of profile failed with: " .. io_err
|
|
end
|
|
local content, err = serialize_profile(profile, format, filter)
|
|
if not content then
|
|
output:close()
|
|
return false, "Saving of profile failed with: " .. err
|
|
end
|
|
output:write(content)
|
|
output:close()
|
|
|
|
local logmessage = "Profile saved to " .. path
|
|
core.log("action", logmessage)
|
|
return true, logmessage
|
|
end
|
|
|
|
return reporter
|