Skip to content

Commit e0e4a7c

Browse files
committed
player/lua/defaults: add LuaJIT profiling support
gives us a way to properly profile the lua scripts to get some verifiable way to test which "optimizations" work and which don't. currently only supports LuaJIT but the cli option is generically named in case support for profiling other scripts is added in the future.
1 parent 5921fe5 commit e0e4a7c

4 files changed

Lines changed: 120 additions & 0 deletions

File tree

DOCS/man/options.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -833,6 +833,13 @@ Program Behavior
833833
and overwrites the internal list with it. The latter is a key/value list
834834
option. See `List Options`_ for details.
835835

836+
``--profile-script=<name[=path]>``, ``--profile-scripts=name1[=path1],name2[=path2],...``
837+
Profile the given script(s) and dump a report to ``path`` when the script
838+
exits. If no ``path`` is given, logs to the console with ``info`` message
839+
level.
840+
841+
Currently only supports profiling lua scripts with LuaJIT.
842+
836843
``--merge-files``
837844
Pretend that all files passed to mpv are concatenated into a single, big
838845
file. This uses timeline/EDL support internally.

options/options.c

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -562,6 +562,8 @@ static const m_option_t mp_opts[] = {
562562
{"js-memory-report", OPT_BOOL(js_memory_report)},
563563
#endif
564564
#if HAVE_LUA
565+
{"profile-scripts", OPT_STRINGLIST(profile_scripts)},
566+
{"profile-script", OPT_CLI_ALIAS("profile-scripts-append")},
565567
{"osc", OPT_BOOL(lua_load_osc), .flags = UPDATE_BUILTIN_SCRIPTS},
566568
{"ytdl", OPT_BOOL(lua_load_ytdl), .flags = UPDATE_BUILTIN_SCRIPTS},
567569
{"ytdl-format", OPT_STRING(lua_ytdl_format)},

options/options.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,7 @@ typedef struct MPOpts {
180180
char **reset_options;
181181
char **script_files;
182182
char **script_opts;
183+
char **profile_scripts;
183184
bool js_memory_report;
184185
bool lua_load_osc;
185186
bool lua_load_ytdl;

player/lua/defaults.lua

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,113 @@ local function dispatch_key_binding(name, state, key_name, key_text, scale, arg)
6767
end
6868
end
6969

70+
-- LuaJIT profiling support
71+
local script_profile = nil
72+
73+
local function get_profile_path()
74+
local scripts = mp.get_property_native("options/profile-scripts", {}) or {}
75+
for i = #scripts, 1, -1 do
76+
local entry = scripts[i]
77+
local name, path = entry:match("^([^=]+)=(.*)$")
78+
if not name then
79+
name = entry
80+
end
81+
if name == mp.script_name then
82+
return path or ""
83+
end
84+
end
85+
return nil
86+
end
87+
88+
local function format_script_profile(state)
89+
local entries = {}
90+
for stack, samples in pairs(state.entries) do
91+
entries[#entries + 1] = {stack = stack, samples = samples}
92+
end
93+
94+
table.sort(entries, function(a, b)
95+
if a.samples ~= b.samples then
96+
return a.samples > b.samples
97+
end
98+
return a.stack < b.stack
99+
end)
100+
101+
local lines = {
102+
"script: " .. mp.script_name,
103+
"samples: " .. state.total_samples,
104+
"",
105+
}
106+
107+
for _, entry in ipairs(entries) do
108+
local percent = entry.samples * 100 / (state.total_samples or 1)
109+
lines[#lines + 1] = string.format("%8d %6.2f%% %s",
110+
entry.samples, percent, entry.stack)
111+
end
112+
113+
return table.concat(lines, "\n")
114+
end
115+
116+
local function write_script_profile(outpath, data)
117+
if outpath == "" then
118+
mp.log("info", data)
119+
return
120+
end
121+
122+
local out, err = io.open(outpath, "w+b")
123+
local ok = out ~= nil
124+
if out ~= nil then
125+
ok, err = out:write(data, "\n")
126+
if ok then
127+
ok, err = out:flush()
128+
end
129+
out:close()
130+
end
131+
if ok then
132+
mp.log("info", "Profile data written to " .. outpath)
133+
else
134+
mp.log("error", "Could not write profile data to " ..
135+
outpath .. ": " .. tostring(err))
136+
end
137+
end
138+
139+
local function stop_script_profile()
140+
if not script_profile then
141+
return
142+
end
143+
144+
script_profile.profiler.stop()
145+
146+
local data = format_script_profile(script_profile)
147+
write_script_profile(script_profile.output_path, data)
148+
script_profile = nil
149+
end
150+
151+
local function setup_script_profile()
152+
local output_path = get_profile_path()
153+
if not output_path then
154+
return
155+
end
156+
157+
local ok, profiler = pcall(require, "jit.profile")
158+
if not ok then
159+
error("Lua profiling requires LuaJIT")
160+
end
161+
162+
script_profile = {
163+
profiler = profiler,
164+
total_samples = 0,
165+
entries = {},
166+
output_path = output_path,
167+
}
168+
169+
profiler.start("fi4", function(thread, samples)
170+
local stack = profiler.dumpstack(thread, "fZ;", 100)
171+
script_profile.entries[stack] =
172+
(script_profile.entries[stack] or 0) + samples
173+
script_profile.total_samples = script_profile.total_samples + samples
174+
end)
175+
end
176+
70177
-- "Old", deprecated API
71178

72179
-- each script has its own section, so that they don't conflict
@@ -421,6 +528,7 @@ mp.keep_running = true
421528

422529
function _G.exit()
423530
mp.keep_running = false
531+
stop_script_profile()
424532
end
425533

426534
local event_handlers = {}
@@ -502,6 +610,8 @@ _G.print = mp.msg.info
502610
package.loaded["mp"] = mp
503611
package.loaded["mp.msg"] = mp.msg
504612

613+
setup_script_profile()
614+
505615
_G.mp_event_loop = function()
506616
mp.dispatch_events(true)
507617
end

0 commit comments

Comments
 (0)