2 --Copyright (C) 2016 T4im
4 --This program is free software; you can redistribute it and/or modify
5 --it under the terms of the GNU Lesser General Public License as published by
6 --the Free Software Foundation; either version 2.1 of the License, or
7 --(at your option) any later version.
9 --This program is distributed in the hope that it will be useful,
10 --but WITHOUT ANY WARRANTY; without even the implied warranty of
11 --MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 --GNU Lesser General Public License for more details.
14 --You should have received a copy of the GNU Lesser General Public License along
15 --with this program; if not, write to the Free Software Foundation, Inc.,
16 --51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
18 local S = core.get_translator("__builtin")
19 -- Note: In this file, only messages are translated
20 -- but not the table itself, to keep it simple.
22 local DIR_DELIM, LINE_DELIM = DIR_DELIM, "\n"
23 local table, unpack, string, pairs, io, os = table, unpack, string, pairs, io, os
24 local rep, sprintf, tonumber = string.rep, string.format, tonumber
25 local core, settings = core, core.settings
29 -- Shorten a string. End on an ellipsis if shortened.
31 local function shorten(str, length)
32 if str and str:len() > length then
33 return "..." .. str:sub(-(length-3))
38 local function filter_matches(filter, text)
39 return not filter or string.match(text, filter)
42 local function format_number(number, fmt)
43 number = tonumber(number)
47 return sprintf(fmt or "%d", number)
51 new = function(self, object)
53 object.out = {} -- output buffer
55 return setmetatable(object, self)
57 __tostring = function (self)
58 return table.concat(self.out, LINE_DELIM)
60 print = function(self, text, ...)
62 text = sprintf(text, ...)
66 -- Avoid format unicode issues.
67 text = text:gsub("Ms", "µs")
70 table.insert(self.out, text or LINE_DELIM)
72 flush = function(self)
73 table.insert(self.out, LINE_DELIM)
74 local text = table.concat(self.out, LINE_DELIM)
80 local widths = { 55, 9, 9, 9, 5, 5, 5 }
81 local txt_row_format = sprintf(" %%-%ds | %%%ds | %%%ds | %%%ds | %%%ds | %%%ds | %%%ds", unpack(widths))
85 HR[i]= rep("-", widths[i])
87 -- ' | ' should break less with github than '-+-', when people are pasting there
88 HR = sprintf("-%s-", table.concat(HR, " | "))
90 local TxtFormatter = Formatter:new {
91 format_row = function(self, modname, instrument_name, statistics)
93 if instrument_name then
94 label = shorten(instrument_name, widths[1] - 5)
95 label = sprintf(" - %s %s", label, rep(".", widths[1] - 5 - label:len()))
96 else -- Print mod_stats
97 label = shorten(modname, widths[1] - 2) .. ":"
100 self:print(txt_row_format, label,
101 format_number(statistics.time_min),
102 format_number(statistics.time_max),
103 format_number(statistics:get_time_avg()),
104 format_number(statistics.part_min, "%.1f"),
105 format_number(statistics.part_max, "%.1f"),
106 format_number(statistics:get_part_avg(), "%.1f")
109 format = function(self, filter)
110 local profile = self.profile
111 self:print(S("Values below show absolute/relative times spend per server step by the instrumented function."))
112 self:print(S("A total of @1 sample(s) were taken.", profile.stats_total.samples))
115 self:print(S("The output is limited to '@1'.", filter))
121 "instrumentation", "min Ms", "max Ms", "avg Ms", "min %", "max %", "avg %"
124 for modname,mod_stats in pairs(profile.stats) do
125 if filter_matches(filter, modname) then
126 self:format_row(modname, nil, mod_stats)
128 if mod_stats.instruments ~= nil then
129 for instrument_name, instrument_stats in pairs(mod_stats.instruments) do
130 self:format_row(nil, instrument_name, instrument_stats)
137 self:format_row("total", nil, profile.stats_total)
142 local CsvFormatter = Formatter:new {
143 format_row = function(self, modname, instrument_name, statistics)
145 "%q,%q,%d,%d,%d,%d,%d,%f,%f,%f",
146 modname, instrument_name,
150 statistics:get_time_avg(),
154 statistics:get_part_avg()
157 format = function(self, filter)
159 "%q,%q,%q,%q,%q,%q,%q,%q,%q,%q",
160 "modname", "instrumentation",
170 for modname, mod_stats in pairs(self.profile.stats) do
171 if filter_matches(filter, modname) then
172 self:format_row(modname, "*", mod_stats)
174 if mod_stats.instruments ~= nil then
175 for instrument_name, instrument_stats in pairs(mod_stats.instruments) do
176 self:format_row(modname, instrument_name, instrument_stats)
184 local function format_statistics(profile, format, filter)
186 if format == "csv" then
187 formatter = CsvFormatter:new {
191 formatter = TxtFormatter:new {
195 formatter:format(filter)
196 return formatter:flush()
200 -- Format the profile ready for display and
201 -- @return string to be printed to the console
203 function reporter.print(profile, filter)
204 if filter == "" then filter = nil end
205 return format_statistics(profile, "txt", filter)
209 -- Serialize the profile data and
210 -- @return serialized data to be saved to a file
212 local function serialize_profile(profile, format, filter)
213 if format == "lua" or format == "json" or format == "json_pretty" then
214 local stats = filter and {} or profile.stats
216 for modname, mod_stats in pairs(profile.stats) do
217 if filter_matches(filter, modname) then
218 stats[modname] = mod_stats
222 if format == "lua" then
223 return core.serialize(stats)
224 elseif format == "json" then
225 return core.write_json(stats)
226 elseif format == "json_pretty" then
227 return core.write_json(stats, true)
230 -- Fall back to textual formats.
231 return format_statistics(profile, format, filter)
234 local worldpath = core.get_worldpath()
235 local function get_save_path(format, filter)
236 local report_path = settings:get("profiler.report_path") or ""
237 if report_path ~= "" then
238 core.mkdir(sprintf("%s%s%s", worldpath, DIR_DELIM, report_path))
241 "%s/%s/profile-%s%s.%s",
244 os.date("%Y%m%dT%H%M%S"),
245 filter and ("-" .. filter) or "",
247 ):gsub("[/\\]+", DIR_DELIM))-- Clean up delims
251 -- Save the profile to the world path.
252 -- @return success, log message
254 function reporter.save(profile, format, filter)
255 if not format or format == "" then
256 format = settings:get("profiler.default_report_format") or "txt"
262 local path = get_save_path(format, filter)
264 local output, io_err = io.open(path, "w")
266 return false, S("Saving of profile failed: @1", io_err)
268 local content, err = serialize_profile(profile, format, filter)
271 return false, S("Saving of profile failed: @1", err)
273 output:write(content)
276 core.log("action", "Profile saved to " .. path)
277 return true, S("Profile saved to @1", path)