Skip to content
Open
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
1 change: 1 addition & 0 deletions DOCS/interface-changes/profile-scripts.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
add `--profile-scripts` for script profiling
15 changes: 15 additions & 0 deletions DOCS/man/options.rst
Original file line number Diff line number Diff line change
Expand Up @@ -833,6 +833,21 @@ Program Behavior
and overwrites the internal list with it. The latter is a key/value list
option. See `List Options`_ for details.

``--profile-script=<name[=path]>``, ``--profile-scripts=name1[=path1],name2[=path2],...``
Profile the given script(s) and dump a report to ``path`` when the script
exits. If no ``path`` is given, logs to the console with ``info`` message
level.

Currently only supports profiling lua scripts with LuaJIT.

The exact format of the report depends on the profiler used. For LuaJIT the
format is::

<Number of samples> <Percentage of total samples> <Stack trace>

Stack is only printed up to a depth of 100. The requested sampling interval
is set to 4ms (but the actual sampling interval may vary due to OS).

``--merge-files``
Pretend that all files passed to mpv are concatenated into a single, big
file. This uses timeline/EDL support internally.
Expand Down
2 changes: 2 additions & 0 deletions options/options.c
Original file line number Diff line number Diff line change
Expand Up @@ -562,6 +562,8 @@ static const m_option_t mp_opts[] = {
{"js-memory-report", OPT_BOOL(js_memory_report)},
#endif
#if HAVE_LUA
{"profile-scripts", OPT_STRINGLIST(profile_scripts)},
{"profile-script", OPT_CLI_ALIAS("profile-scripts-append")},
{"osc", OPT_BOOL(lua_load_osc), .flags = UPDATE_BUILTIN_SCRIPTS},
{"ytdl", OPT_BOOL(lua_load_ytdl), .flags = UPDATE_BUILTIN_SCRIPTS},
{"ytdl-format", OPT_STRING(lua_ytdl_format)},
Expand Down
1 change: 1 addition & 0 deletions options/options.h
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ typedef struct MPOpts {
char **reset_options;
char **script_files;
char **script_opts;
char **profile_scripts;
bool js_memory_report;
bool lua_load_osc;
bool lua_load_ytdl;
Expand Down
110 changes: 110 additions & 0 deletions player/lua/defaults.lua
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,113 @@ local function dispatch_key_binding(name, state, key_name, key_text, scale, arg)
end
end

-- LuaJIT profiling support
local script_profile = nil

local function get_profile_path()
local scripts = mp.get_property_native("options/profile-scripts", {}) or {}
for i = #scripts, 1, -1 do
local entry = scripts[i]
local name, path = entry:match("^([^=]+)=(.*)$")
if not name then
name = entry
end
if name == mp.script_name then
return path or ""
end
end
return nil
end

local function format_script_profile(state)
local entries = {}
for stack, samples in pairs(state.entries) do
entries[#entries + 1] = {stack = stack, samples = samples}
end

table.sort(entries, function(a, b)
if a.samples ~= b.samples then
return a.samples > b.samples
end
return a.stack < b.stack
end)

local lines = {
"script: " .. mp.script_name,
"samples: " .. state.total_samples,
"",
}

for _, entry in ipairs(entries) do
local percent = entry.samples * 100 / (state.total_samples or 1)
lines[#lines + 1] = string.format("%8d %6.2f%% %s",
entry.samples, percent, entry.stack)
end

return table.concat(lines, "\n")
end

local function write_script_profile(outpath, data)
if outpath == "" then
mp.log("info", data)
return
end

local out, err = io.open(outpath, "w+b")
local ok = out ~= nil
if out ~= nil then
ok, err = out:write(data, "\n")
if ok then
ok, err = out:flush()
end
out:close()
end
if ok then
mp.log("info", "Profile data written to " .. outpath)
else
mp.log("error", "Could not write profile data to " ..
outpath .. ": " .. tostring(err))
end
end

local function stop_script_profile()
if not script_profile then
return
end

script_profile.profiler.stop()

local data = format_script_profile(script_profile)
write_script_profile(script_profile.output_path, data)
script_profile = nil
end

local function setup_script_profile()
local output_path = get_profile_path()
if not output_path then
return
end

local ok, profiler = pcall(require, "jit.profile")
if not ok then
error("Lua profiling requires LuaJIT")
end

script_profile = {
profiler = profiler,
total_samples = 0,
entries = {},
output_path = output_path,
}

profiler.start("fi4", function(thread, samples)
local stack = profiler.dumpstack(thread, "fZ;", 100)
script_profile.entries[stack] =
(script_profile.entries[stack] or 0) + samples
script_profile.total_samples = script_profile.total_samples + samples
end)
end

-- "Old", deprecated API

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

function _G.exit()
mp.keep_running = false
stop_script_profile()
end

local event_handlers = {}
Expand Down Expand Up @@ -502,6 +610,8 @@ _G.print = mp.msg.info
package.loaded["mp"] = mp
package.loaded["mp.msg"] = mp.msg

setup_script_profile()

_G.mp_event_loop = function()
mp.dispatch_events(true)
end
Expand Down
Loading