diff --git a/DOCS/interface-changes/profile-scripts.rst b/DOCS/interface-changes/profile-scripts.rst new file mode 100644 index 0000000000000..7201c64198e5a --- /dev/null +++ b/DOCS/interface-changes/profile-scripts.rst @@ -0,0 +1 @@ +add `--profile-scripts` for script profiling diff --git a/DOCS/man/options.rst b/DOCS/man/options.rst index 0c06a3d22a59e..6843fccadc2ad 100644 --- a/DOCS/man/options.rst +++ b/DOCS/man/options.rst @@ -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=``, ``--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:: + + + + 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. diff --git a/options/options.c b/options/options.c index 9639294f1c382..ba1ebd27a4ca4 100644 --- a/options/options.c +++ b/options/options.c @@ -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)}, diff --git a/options/options.h b/options/options.h index a8f82a98103e0..4f6c51c6e477a 100644 --- a/options/options.h +++ b/options/options.h @@ -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; diff --git a/player/lua/defaults.lua b/player/lua/defaults.lua index c85bbe0bd2671..f8ddfcea07f8d 100644 --- a/player/lua/defaults.lua +++ b/player/lua/defaults.lua @@ -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 @@ -421,6 +528,7 @@ mp.keep_running = true function _G.exit() mp.keep_running = false + stop_script_profile() end local event_handlers = {} @@ -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