diff --git a/res/gamedata/scripts/profiler.script b/res/gamedata/scripts/profiler.script new file mode 100644 index 00000000000..26eb4d733c1 --- /dev/null +++ b/res/gamedata/scripts/profiler.script @@ -0,0 +1,11 @@ +-- Original profiler was not working and caused game to crash when jit parameter is disabled. +-- For safety reasons there is mock setup function and empty module without original code. +-- +-- 1) LUA does not allow setting up multiple hooks, adding hooks here instead of C++ codebase is problematic +-- 2) Luabind/Luajit already injects complex logics, C++ codebase adds error callbacks and custom logics. +-- As result, stack and state may be corrupted and cause random logics/memory errors. +-- +-- See: https://github.com/OpenXRay/xray-16/issues/1436 + +-- Mock placeholder. +function setup_hook() end diff --git a/src/xrGame/ai_space.cpp b/src/xrGame/ai_space.cpp index aef1fd71c32..74dfe403e9e 100644 --- a/src/xrGame/ai_space.cpp +++ b/src/xrGame/ai_space.cpp @@ -50,7 +50,7 @@ void CAI_Space::init() m_moving_objects = xr_make_unique<::moving_objects>(); VERIFY(!GEnv.ScriptEngine); - GEnv.ScriptEngine = xr_new(); + GEnv.ScriptEngine = xr_new(false, true); RestartScriptEngine(); } diff --git a/src/xrGame/console_commands.cpp b/src/xrGame/console_commands.cpp index 09adc0af537..199dc431f52 100644 --- a/src/xrGame/console_commands.cpp +++ b/src/xrGame/console_commands.cpp @@ -1992,6 +1992,135 @@ class CCC_UI_Time_Factor : public IConsole_Command } }; +class CCC_LuaProfiler : public IConsole_Command +{ +public: + constexpr static cpcstr COMMAND_LUA_PROFILER_STATUS = "lua_profiler_status"; + constexpr static cpcstr COMMAND_LUA_PROFILER_START = "lua_profiler_start"; + constexpr static cpcstr COMMAND_LUA_PROFILER_START_HOOK_MODE = "lua_profiler_start_hook_mode"; + constexpr static cpcstr COMMAND_LUA_PROFILER_START_SAMPLING_MODE = "lua_profiler_start_sampling_mode"; + constexpr static cpcstr COMMAND_LUA_PROFILER_STOP = "lua_profiler_stop"; + constexpr static cpcstr COMMAND_LUA_PROFILER_RESET = "lua_profiler_reset"; + constexpr static cpcstr COMMAND_LUA_PROFILER_LOG = "lua_profiler_log"; + constexpr static cpcstr COMMAND_LUA_PROFILER_SAVE = "lua_profiler_save"; + + CCC_LuaProfiler(LPCSTR N) : IConsole_Command(N) { bEmptyArgsHandled = true; }; + virtual void Execute(LPCSTR args) + { + CScriptProfiler* profiler = GEnv.ScriptEngine->m_profiler; + + if (strstr(cName, COMMAND_LUA_PROFILER_STATUS) == cName) + { + Msg("[P] Profiler status: %s, type - %s", profiler->isActive() ? "on" : "off", + profiler->getTypeString().c_str()); + } + else if (strstr(cName, COMMAND_LUA_PROFILER_START_HOOK_MODE) == cName) + { + profiler->startHookMode(); + } + else if (strstr(cName, COMMAND_LUA_PROFILER_START_SAMPLING_MODE) == cName) + { + u32 interval = atoi(args); + + profiler->startSamplingMode(interval ? interval : CScriptProfiler::PROFILE_SAMPLING_INTERVAL_DEFAULT); + } + else if (strstr(cName, COMMAND_LUA_PROFILER_START) == cName) + { + u32 profiler_type = atoi(args); + + profiler->start( + (profiler_type ? (CScriptProfilerType)profiler_type : CScriptProfiler::PROFILE_TYPE_DEFAULT)); + } + else if (strstr(cName, COMMAND_LUA_PROFILER_STOP) == cName) + { + profiler->stop(); + } + else if (strstr(cName, COMMAND_LUA_PROFILER_RESET) == cName) + { + profiler->reset(); + } + else if (strstr(cName, COMMAND_LUA_PROFILER_LOG) == cName) + { + u32 limit = atoi(args); + + profiler->logReport(limit ? limit : CScriptProfiler::PROFILE_ENTRIES_LOG_LIMIT_DEFAULT); + } + else if (strstr(cName, COMMAND_LUA_PROFILER_SAVE) == cName) + { + profiler->saveReport(); + } + }; + + void fill_tips(vecTips& tips, u32 /*mode*/) override + { + CScriptProfiler* profiler = GEnv.ScriptEngine->m_profiler; + TStatus status_buffer; + + if (strstr(cName, COMMAND_LUA_PROFILER_STATUS) == cName) + { + // No arguments. + } + else if (strstr(cName, COMMAND_LUA_PROFILER_START_HOOK_MODE) == cName) + { + // No arguments. + } + else if (strstr(cName, COMMAND_LUA_PROFILER_START_SAMPLING_MODE) == cName) + { + xr_sprintf(status_buffer, "%d (default) [1-%d] - sampling interval", + CScriptProfiler::PROFILE_SAMPLING_INTERVAL_DEFAULT, CScriptProfiler::PROFILE_SAMPLING_INTERVAL_MAX); + tips.push_back(status_buffer); + } + else if (strstr(cName, COMMAND_LUA_PROFILER_START) == cName) + { + xr_sprintf(status_buffer, "%d - hooks based profiler", CScriptProfilerType::Hook); + tips.push_back(status_buffer); + + xr_sprintf(status_buffer, "%d - sampling based profiler", CScriptProfilerType::Sampling); + tips.push_back(status_buffer); + } + else if (strstr(cName, COMMAND_LUA_PROFILER_STOP) == cName) + { + // No arguments. + } + else if (strstr(cName, COMMAND_LUA_PROFILER_RESET) == cName) + { + // No arguments. + } + else if (strstr(cName, COMMAND_LUA_PROFILER_LOG) == cName) + { + xr_sprintf(status_buffer, "%d (default) - count of profiling entries to print", + CScriptProfiler::PROFILE_ENTRIES_LOG_LIMIT_DEFAULT, CScriptProfiler::PROFILE_SAMPLING_INTERVAL_MAX); + tips.push_back(status_buffer); + } + else if (strstr(cName, COMMAND_LUA_PROFILER_SAVE) == cName) + { + // No arguments. + } + } + + void Info(TInfo& I) override + { + if (strstr(cName, COMMAND_LUA_PROFILER_STATUS) == cName) + xr_strcpy(I, "no arguments : print lua profiler status"); + else if (strstr(cName, COMMAND_LUA_PROFILER_START_HOOK_MODE) == cName) + xr_strcpy(I, "no arguments : start lua script profiling in hook mode"); + else if (strstr(cName, COMMAND_LUA_PROFILER_START_SAMPLING_MODE) == cName) + xr_strcpy(I, + "integer value in range [1,1000] : start lua script profiling in sampling mode with provided sampling " + "interval"); + else if (strstr(cName, COMMAND_LUA_PROFILER_START) == cName) + xr_strcpy(I, "integer value in range [0,2] : start lua script profiling in provided mode"); + else if (strstr(cName, COMMAND_LUA_PROFILER_STOP) == cName) + xr_strcpy(I, "no arguments : stop lua script profiling"); + else if (strstr(cName, COMMAND_LUA_PROFILER_RESET) == cName) + xr_strcpy(I, "no arguments : reset lua script profiling stats"); + else if (strstr(cName, COMMAND_LUA_PROFILER_LOG) == cName) + xr_strcpy(I, "integer value : log lua script profiling stats, limit entries with argument"); + else if (strstr(cName, COMMAND_LUA_PROFILER_SAVE) == cName) + xr_strcpy(I, "no arguments : save lua script profiling stats in a file"); + } +}; + void CCC_RegisterCommands() { ZoneScoped; @@ -2076,6 +2205,15 @@ void CCC_RegisterCommands() CMD3(CCC_Mask, "lua_debug", &g_LuaDebug, 1); #endif // MASTER_GOLD + CMD1(CCC_LuaProfiler, CCC_LuaProfiler::COMMAND_LUA_PROFILER_STATUS); + CMD1(CCC_LuaProfiler, CCC_LuaProfiler::COMMAND_LUA_PROFILER_START); + CMD1(CCC_LuaProfiler, CCC_LuaProfiler::COMMAND_LUA_PROFILER_START_SAMPLING_MODE); + CMD1(CCC_LuaProfiler, CCC_LuaProfiler::COMMAND_LUA_PROFILER_START_HOOK_MODE); + CMD1(CCC_LuaProfiler, CCC_LuaProfiler::COMMAND_LUA_PROFILER_STOP); + CMD1(CCC_LuaProfiler, CCC_LuaProfiler::COMMAND_LUA_PROFILER_RESET); + CMD1(CCC_LuaProfiler, CCC_LuaProfiler::COMMAND_LUA_PROFILER_LOG); + CMD1(CCC_LuaProfiler, CCC_LuaProfiler::COMMAND_LUA_PROFILER_SAVE); + #ifdef DEBUG CMD4(CCC_Integer, "lua_gcstep", &psLUA_GCSTEP, 1, 1000); CMD3(CCC_Mask, "ai_debug", &psAI_Flags, aiDebug); diff --git a/src/xrScriptEngine/CMakeLists.txt b/src/xrScriptEngine/CMakeLists.txt index ead48c5ce2b..8383d9bfff7 100644 --- a/src/xrScriptEngine/CMakeLists.txt +++ b/src/xrScriptEngine/CMakeLists.txt @@ -14,6 +14,9 @@ target_sources_grouped( script_debugger_threads.hpp script_lua_helper.cpp script_lua_helper.hpp + script_profiler.cpp + script_profiler.hpp + script_profiler_portions.hpp ) target_sources_grouped( diff --git a/src/xrScriptEngine/ScriptEngineScript.cpp b/src/xrScriptEngine/ScriptEngineScript.cpp index ad0303425f9..0d771a72882 100644 --- a/src/xrScriptEngine/ScriptEngineScript.cpp +++ b/src/xrScriptEngine/ScriptEngineScript.cpp @@ -1,9 +1,9 @@ //////////////////////////////////////////////////////////////////////////// -// Module : script_engine_script.cpp -// Created : 25.12.2002 -// Modified : 13.05.2004 -// Author : Dmitriy Iassenev -// Description : ALife Simulator script engine export +// Module : script_engine_script.cpp +// Created : 25.12.2002 +// Modified : 13.05.2004 +// Author : Dmitriy Iassenev +// Description : ALife Simulator script engine export //////////////////////////////////////////////////////////////////////////// #include "pch.hpp" @@ -139,6 +139,11 @@ std::ostream& operator<<(std::ostream& os, const profile_timer_script& pt) { ret SCRIPT_EXPORT(CScriptEngine, (), { using namespace luabind; + + globals(luaState) ["PROFILER_TYPE_NONE"] = (u32) CScriptProfilerType::None; + globals(luaState) ["PROFILER_TYPE_HOOK"] = (u32) CScriptProfilerType::Hook; + globals(luaState) ["PROFILER_TYPE_SAMPLING"] = (u32) CScriptProfilerType::Sampling; + module(luaState) [ class_("profile_timer") @@ -164,4 +169,74 @@ SCRIPT_EXPORT(CScriptEngine, (), def("editor", &is_editor), def("user_name", &user_name) ]; + + module(luaState, "profiler") + [ + def("is_active", +[]() -> bool + { + return GEnv.ScriptEngine->m_profiler->isActive(); + }), + def("get_type", +[]()-> u32 + { + return static_cast(GEnv.ScriptEngine->m_profiler->getType()); + }), + def("start", +[]() + { + GEnv.ScriptEngine->m_profiler->start(); + }), + def("start", +[](CScriptProfilerType profiler_type) + { + GEnv.ScriptEngine->m_profiler->start(profiler_type); + }), + def("start_hook_mode", +[]() + { + GEnv.ScriptEngine->m_profiler->startHookMode(); + }), + def("start_sampling_mode", +[]() + { + GEnv.ScriptEngine->m_profiler->startSamplingMode(); + }), + def("start_sampling_mode", +[](u32 sampling_interval = CScriptProfiler::PROFILE_SAMPLING_INTERVAL_DEFAULT) + { + GEnv.ScriptEngine->m_profiler->startSamplingMode(sampling_interval); + }), + def("stop", +[]() + { + GEnv.ScriptEngine->m_profiler->stop(); + }), + def("reset", +[]() + { + GEnv.ScriptEngine->m_profiler->reset(); + }), + def("log_report", +[]() + { + GEnv.ScriptEngine->m_profiler->logReport(); + }), + def("log_report", +[](u32 entries_limit) + { + GEnv.ScriptEngine->m_profiler->logReport(entries_limit); + }), + def("save_report", +[]() + { + GEnv.ScriptEngine->m_profiler->saveReport(); + }) + ]; + + /** + * Exports injected from tracy profiler: + * + * https://github.com/wolfpld/tracy/blob/da60684b9f61b34afa5aa243a7838d6e79096783/manual/tracy.tex#L1932 + * https://github.com/wolfpld/tracy/blob/da60684b9f61b34afa5aa243a7838d6e79096783/public/tracy/TracyLua.hpp#L18 + * + * global tracy { + * function ZoneBegin; + * function ZoneBeginN; + * function ZoneBeginS; + * function ZoneBeginNS; + * function ZoneEnd; + * function ZoneText; + * function ZoneName; + * function Message; + * } + */ }); diff --git a/src/xrScriptEngine/script_debugger.cpp b/src/xrScriptEngine/script_debugger.cpp index 0e5020ec68f..2f285a84b56 100644 --- a/src/xrScriptEngine/script_debugger.cpp +++ b/src/xrScriptEngine/script_debugger.cpp @@ -114,9 +114,9 @@ BOOL CScriptDebugger::Active() { return m_bIdePresent; } CScriptDebugger::CScriptDebugger(CScriptEngine* scriptEngine) { this->scriptEngine = scriptEngine; - m_threads = new CDbgScriptThreads(scriptEngine, this); - m_callStack = new CScriptCallStack(this); - m_lua = new CDbgLuaHelper(this); + m_threads = xr_new(scriptEngine, this); + m_callStack = xr_new(this); + m_lua = xr_new(this); ZeroMemory(m_curr_connected_mslot, sizeof(m_curr_connected_mslot)); // m_pDebugger = this; m_nLevel = 0; diff --git a/src/xrScriptEngine/script_engine.cpp b/src/xrScriptEngine/script_engine.cpp index e5d725a0f0d..931c1f7c0b4 100644 --- a/src/xrScriptEngine/script_engine.cpp +++ b/src/xrScriptEngine/script_engine.cpp @@ -9,12 +9,16 @@ #include "pch.hpp" #include "script_engine.hpp" #include "script_process.hpp" +#include "script_profiler.hpp" #include "script_thread.hpp" #include "ScriptExporter.hpp" #include "BindingsDumper.hpp" #ifdef USE_DEBUGGER #include "script_debugger.hpp" #endif +#ifdef DEBUG +#include "script_thread.hpp" +#endif #include #include "Common/Noncopyable.hpp" #include "xrCore/ModuleLookup.hpp" @@ -114,6 +118,9 @@ void CScriptEngine::reinit() stateMapLock.Leave(); if (m_virtual_machine) { + if (m_profiler) + m_profiler->onDispose(m_virtual_machine); + lua_close(m_virtual_machine); UnregisterState(m_virtual_machine); } @@ -130,6 +137,9 @@ void CScriptEngine::reinit() file_header = file_header_old; scriptBufferSize = 1024 * 1024; scriptBuffer = xr_alloc(scriptBufferSize); + + if (m_profiler) + m_profiler->onReinit(m_virtual_machine); } void CScriptEngine::print_stack(lua_State* L) @@ -747,13 +757,14 @@ void CScriptEngine::disconnect_from_debugger() } #endif -CScriptEngine::CScriptEngine(bool is_editor) +CScriptEngine::CScriptEngine(bool is_editor, bool is_with_profiler) { luabind::allocator = &luabind_allocator; luabind::allocator_context = nullptr; m_current_thread = nullptr; m_stack_is_ready = false; m_virtual_machine = nullptr; + m_profiler = is_with_profiler && !is_editor ? xr_new(this) : nullptr; m_stack_level = 0; m_reload_modules = false; m_last_no_file_length = 0; @@ -773,6 +784,14 @@ CScriptEngine::CScriptEngine(bool is_editor) CScriptEngine::~CScriptEngine() { + if (m_profiler) + { + if (m_virtual_machine) + m_profiler->onDispose(m_virtual_machine); + + xr_delete(m_profiler); + } + if (m_virtual_machine) lua_close(m_virtual_machine); while (!m_script_processes.empty()) @@ -871,19 +890,21 @@ void CScriptEngine::setup_callbacks() lua_atpanic(lua(), CScriptEngine::lua_panic); } -#ifdef DEBUG -#include "script_thread.hpp" - void CScriptEngine::lua_hook_call(lua_State* L, lua_Debug* dbg) { CScriptEngine* scriptEngine = GetInstance(L); VERIFY(scriptEngine); + + #ifdef DEBUG if (scriptEngine->current_thread()) scriptEngine->current_thread()->script_hook(L, dbg); else scriptEngine->m_stack_is_ready = true; + #endif + + if (scriptEngine->m_profiler) + scriptEngine->m_profiler->onLuaHookCall(L, dbg); } -#endif int CScriptEngine::auto_load(lua_State* L) { @@ -1025,7 +1046,9 @@ void CScriptEngine::init(ExporterFunc exporterFunc, bool loadGlobalNamespace) // if (jit == nil) then // profiler.setup_hook() // end - if (!strstr(Core.Params, "-nojit")) + // + // Update: '-nojit' option adds garbage to stack and luabind calls fail + if (!strstr(Core.Params, ARGUMENT_ENGINE_NOJIT)) { luajit::open_lib(lua(), LUA_JITLIBNAME, luaopen_jit); // Xottab_DUTY: commented this. Let's use default opt level, which is 3 diff --git a/src/xrScriptEngine/script_engine.hpp b/src/xrScriptEngine/script_engine.hpp index 8da5f8e49f3..c0150567b1c 100644 --- a/src/xrScriptEngine/script_engine.hpp +++ b/src/xrScriptEngine/script_engine.hpp @@ -1,15 +1,16 @@ //////////////////////////////////////////////////////////////////////////// -// Module : script_engine.h -// Created : 01.04.2004 -// Modified : 01.04.2004 -// Author : Dmitriy Iassenev -// Description : XRay Script Engine +// Module : script_engine.h +// Created : 01.04.2004 +// Modified : 01.04.2004 +// Author : Dmitriy Iassenev +// Description : XRay Script Engine //////////////////////////////////////////////////////////////////////////// #pragma once #include "xrCore/xrCore.h" #include "xrScriptEngine/xrScriptEngine.hpp" #include "xrScriptEngine/ScriptExporter.hpp" +#include "xrScriptEngine/script_profiler.hpp" #include "xrScriptEngine/script_space_forward.hpp" #include "xrScriptEngine/Functor.hpp" #include "xrCore/Threading/Lock.hpp" @@ -70,6 +71,7 @@ extern Flags32 XRSCRIPTENGINE_API g_LuaDebug; class XRSCRIPTENGINE_API CScriptEngine { public: + constexpr static cpcstr ARGUMENT_ENGINE_NOJIT = "-nojit"; typedef AssociativeVector CScriptProcessStorage; static const char* const GlobalNamespace; @@ -153,6 +155,8 @@ class XRSCRIPTENGINE_API CScriptEngine } public: + CScriptProfiler* m_profiler; + lua_State* lua() { return m_virtual_machine; } void current_thread(CScriptThread* thread) { @@ -218,7 +222,7 @@ class XRSCRIPTENGINE_API CScriptEngine void LogVariable(lua_State* l, pcstr name, int level); using ExporterFunc = XRay::ScriptExporter::Node::ExporterFunc; - CScriptEngine(bool is_editor = false); + CScriptEngine(bool is_editor = false, bool is_with_profiler = false); virtual ~CScriptEngine(); void init(ExporterFunc exporterFunc, bool loadGlobalNamespace); virtual void unload(); @@ -228,9 +232,7 @@ class XRSCRIPTENGINE_API CScriptEngine #if 1 //!XRAY_EXCEPTIONS static void lua_cast_failed(lua_State* L, const luabind::type_id& info); #endif -#ifdef DEBUG static void lua_hook_call(lua_State* L, lua_Debug* dbg); -#endif void setup_callbacks(); bool load_file(const char* scriptName, const char* namespaceName); CScriptProcess* script_process(const ScriptProcessor& process_id) const; diff --git a/src/xrScriptEngine/script_profiler.cpp b/src/xrScriptEngine/script_profiler.cpp new file mode 100644 index 00000000000..0975deb41ff --- /dev/null +++ b/src/xrScriptEngine/script_profiler.cpp @@ -0,0 +1,773 @@ +#include "pch.hpp" +#include "script_profiler.hpp" +#include "xrScriptEngine/script_engine.hpp" + +CScriptProfiler::CScriptProfiler(CScriptEngine* engine) +{ + R_ASSERT(engine != NULL); + + m_engine = engine; + m_active = false; + m_profiler_type = CScriptProfilerType::None; + m_sampling_profile_interval = PROFILE_SAMPLING_INTERVAL_DEFAULT; + + if (strstr(Core.Params, ARGUMENT_PROFILER_DEFAULT)) + start(); + else if (strstr(Core.Params, ARGUMENT_PROFILER_HOOK)) + start(CScriptProfilerType::Hook); + else if (strstr(Core.Params, ARGUMENT_PROFILER_SAMPLING)) + start(CScriptProfilerType::Sampling); +} + +CScriptProfiler::~CScriptProfiler() +{ + m_engine = nullptr; +} + +/* + * @returns current hook type as shared string + */ +shared_str CScriptProfiler::getTypeString() const +{ + switch (m_profiler_type) + { + case CScriptProfilerType::None: + return "None"; + case CScriptProfilerType::Hook: + return "Hook"; + case CScriptProfilerType::Sampling: + return "Sampling"; + default: + NODEFAULT; + return "Unknown"; + } +}; + +/* + * @returns count of recorded profiling entries (based on currently active hook type) + */ +u32 CScriptProfiler::getRecordsCount() const +{ + switch (m_profiler_type) + { + case CScriptProfilerType::None: + return 0; + case CScriptProfilerType::Hook: + return m_hook_profiling_portions.size(); + case CScriptProfilerType::Sampling: + return m_sampling_profiling_log.size(); + default: + NODEFAULT; + return 0; + } +}; + +/* + * Start profiler with provided type. + * + * @param profiler_type - type of the profiler to start + */ +void CScriptProfiler::start(CScriptProfilerType profiler_type) +{ + switch (profiler_type) + { + case CScriptProfilerType::Hook: + startHookMode(); + return; + case CScriptProfilerType::Sampling: + startSamplingMode(PROFILE_SAMPLING_INTERVAL_DEFAULT); + return; + case CScriptProfilerType::None: + Msg("[P] Tried to start none type profiler"); + return; + default: + Msg("[P] Tried to start unknown type (%d) profiler", profiler_type); + return; + } +} + +/* + * Start profiler in hook mode (based on built-in lua tools). + */ +void CScriptProfiler::startHookMode() +{ + if (m_active) + { + Msg("[P] Tried to start already active profiler, operation ignored"); + return; + } + + if (lua()) + { + if (attachLuaHook()) + Msg("[P] Starting scripts hook profiler"); + else + { + Msg("[P] Cannot start scripts hook profiler, hook was not set properly"); + return; + } + } + else + { + Msg("[P] Activating hook profiler on lua engine start, waiting init"); + } + + m_hook_profiling_portions.clear(); + m_profiler_type = CScriptProfilerType::Hook; + m_active = true; +} + +/* + * Start profiler in sampling mode (based on luaJIT built-in profiler). + * + * @param sampling_interval - interval for calls sampling and further reporting + */ +void CScriptProfiler::startSamplingMode(u32 sampling_interval) +{ + if (m_active) + { + Msg("[P] Tried to start already active profiler, operation ignored"); + return; + } + + if (!luaIsJitProfilerDefined()) + { + Msg("[P] Cannot start scripts sampling profiler, jit module is not defined"); + return; + } + + clamp(sampling_interval, 1u, PROFILE_SAMPLING_INTERVAL_MAX); + + if (lua()) + { + Msg("[P] Starting scripts sampling profiler, interval: %d", sampling_interval); + luaJitSamplingProfilerAttach(this, sampling_interval); + } + else + Msg("[P] Activating sampling profiler on lua engine start, waiting init"); + + m_sampling_profiling_log.clear(); + m_sampling_profile_interval = sampling_interval; + m_profiler_type = CScriptProfilerType::Sampling; + m_active = true; +} + +/* + * Stop profiler and clean up stored data. + * Clean up attached hooks/jit profilers. + * + * Note: + * - LUA hook is not detached because we cannot be sure where it was attached, but calls are ignore while profiling stopped + */ +void CScriptProfiler::stop() +{ + if (!m_active) + { + Msg("[P] Tried to stop inactive profiler"); + return; + } + + switch (m_profiler_type) + { + case CScriptProfilerType::Hook: + Msg("[P] Stopping scripts hook profiler"); + // Do not detach hook here, adding it means that it is already test run in the first place. + break; + case CScriptProfilerType::Sampling: + { + Msg("[P] Stopping scripts sampling profiler"); + // Detach profiler from luajit, stop operation will be ignore anyway if it is stopped/captured by another VM. + luaJitProfilerStop(lua()); + break; + } + default: + Msg("[P] Tried to stop none type profiler"); + return; + } + + m_active = false; + m_profiler_type = CScriptProfilerType::None; + m_hook_profiling_portions.clear(); + m_sampling_profiling_log.clear(); +} + +/* + * Reset profiling data. + * Does not affect profiler flow (start, stop, reinit etc). + */ +void CScriptProfiler::reset() +{ + Msg("[P] Reset profiler"); + + m_hook_profiling_portions.clear(); + m_sampling_profiling_log.clear(); +} + +/* + * Log profiler report with brief summary based on current context. + * + * @param entries_limit - count of top entries to log + */ +void CScriptProfiler::logReport(u32 entries_limit) +{ + switch (m_profiler_type) + { + case CScriptProfilerType::Hook: + return logHookReport(entries_limit); + case CScriptProfilerType::Sampling: + return logSamplingReport(entries_limit); + default: + Msg("[P] No active profiling data to report"); + return; + } +} + +/* + * Log hook profiler report with brief summary. + * + * @param entries_limit - count of top entries to log + */ +void CScriptProfiler::logHookReport(u32 entries_limit) +{ + if (m_hook_profiling_portions.empty()) + { + Msg("[P] Nothing to report for hook profiler, data is missing"); + return; + } + + u64 total_count = 0; + u64 total_duration = 0; + + xr_vector entries; + entries.reserve(m_hook_profiling_portions.size()); + + for (auto it = m_hook_profiling_portions.begin(); it != m_hook_profiling_portions.end(); it++) + { + entries.push_back(it); + total_count += it->second.count(); + total_duration += it->second.duration(); + } + + Msg("[P] =================================================================="); + Msg("[P] = Hook profiler report, %d entries", entries.size()); + Msg("[P] =================================================================="); + Msg("[P] = By calls duration:"); + Msg("[P] =================================================================="); + Msg("[P] [idx] sum sum%% avg | calls | trace"); + + u64 index = 0; + + std::sort(entries.begin(), entries.end(), + [](auto& left, auto& right) { return left->second.duration() > right->second.duration(); }); + + for (auto it = entries.begin(); it != entries.end(); it++) + { + if (index >= entries_limit) + break; + + if (total_duration > 0) + Msg("[P] [%3d] %9.3f ms %5.2f%% %9.3f ms | %9d | %s", index, (*it)->second.duration() / 1000.0, + ((f64)(*it)->second.duration() * 100.0) / (f64)total_duration, + (f64)(*it)->second.duration() / (f64)(*it)->second.count() / 1000.0, (*it)->second.count(), + (*it)->first.c_str()); + else + // Small measurements chunks can result in 10-20 calls with ~0 total duration. + Msg("[P] [%3d] %9.3f ms %5.2f%% %9.3f ms | %9d | %s", index, 0, 0, 0, (*it)->second.count(), + (*it)->first.c_str()); + + index += 1; + } + + Msg("[P] =================================================================="); + Msg("[P] = By calls count:"); + Msg("[P] =================================================================="); + Msg("[P] [idx] calls calls%% | trace"); + + index = 0; + + std::sort(entries.begin(), entries.end(), + [](auto& left, auto& right) { return left->second.count() > right->second.count(); }); + + for (auto it = entries.begin(); it != entries.end(); it++) + { + if (index >= entries_limit) + break; + + Msg("[P] [%3d] %9d %5.2f%% | %s", index, (*it)->second.count(), + ((f64)(*it)->second.count() * 100.0) / (f64)total_count, (*it)->first.c_str()); + + index += 1; + } + + Msg("[P] =================================================================="); + Msg("[P] = Total function calls count: %d", total_count); + Msg("[P] = Total function calls duration: %f ms", (f32) total_duration / 1000.0); + Msg("[P] =================================================================="); + + FlushLog(); +} + +/* + * Log sampling profiler report with brief summary. + * + * @param entries_limit - count of top entries to log + */ +void CScriptProfiler::logSamplingReport(u32 entries_limit) +{ + if (m_sampling_profiling_log.empty()) + { + Msg("[P] Nothing to report for sampling profiler, data is missing"); + return; + } + + u64 total_count = 0; + xr_unordered_map sampling_portions; + + for (auto it = m_sampling_profiling_log.begin(); it != m_sampling_profiling_log.end(); it++) + { + auto portion = sampling_portions.find(it->m_name); + + if (portion != sampling_portions.end()) + portion->second.m_samples += it->m_samples; + else + sampling_portions.emplace(it->m_name, it->cloned()); + } + + xr_vector entries; + entries.reserve(sampling_portions.size()); + + for (auto it = sampling_portions.begin(); it != sampling_portions.end(); it++) + { + entries.push_back(it); + total_count += it->second.m_samples; + } + + std::sort(entries.begin(), entries.end(), + [](auto& left, auto& right) { return left->second.m_samples > right->second.m_samples; }); + + Msg("[P] =================================================================="); + Msg("[P] = Sampling profiler report, %d entries", entries.size()); + Msg("[P] =================================================================="); + Msg("[P] [idx] samples %% | trace"); + + u64 index = 0; + + for (auto it = entries.begin(); it != entries.end(); it++) + { + if (index >= entries_limit) + break; + + Msg("[P] [%3d] %9d %5.2f%% | %s", index, (*it)->second.m_samples, + ((f64)(*it)->second.m_samples * 100.0) / (f64)total_count, (*it)->first.c_str()); + + index += 1; + } + + Msg("[P] =================================================================="); + Msg("[P] = Total samples: %d", total_count); + Msg("[P] =================================================================="); + + FlushLog(); +} + +/* + * Save reported data for hook profiler based on current context. + */ +void CScriptProfiler::saveReport() +{ + switch (m_profiler_type) + { + case CScriptProfilerType::Hook: + return saveHookReport(getHookReportFilename()); + case CScriptProfilerType::Sampling: + return saveSamplingReport(getSamplingReportFilename()); + default: + Msg("[P] No active profiling data to save report"); + return; + } +} + +/* + * Save reported data for hook profiler. + * Saved data is represented by lines with serialized stack, calls count and duration. + * + * @param filename - target file to write report to + */ +void CScriptProfiler::saveHookReport(shared_str filename) +{ + if (m_hook_profiling_portions.empty()) + { + Msg("[P] Nothing to report for hook profiler, data is missing"); + return; + } + + Msg("[P] Saving hook report to %s", filename.c_str()); + IWriter* F = FS.w_open(filename.c_str()); + + xr_vector entries; + entries.reserve(m_hook_profiling_portions.size()); + + for (auto it = m_hook_profiling_portions.begin(); it != m_hook_profiling_portions.end(); it++) + entries.push_back(it); + + std::sort(entries.begin(), entries.end(), + [](auto& left, auto& right) { return left->second.duration() > right->second.duration(); }); + + if (F) + { + string2048 buffer; + + for (auto &it : entries) + { + xr_sprintf(buffer, "trace:%s calls:%d avg:%.3fms sum:%.3fms ", it->first.c_str(), it->second.count(), + (f32)it->second.duration() / (f32)it->second.count() / 1000.0, it->second.duration() / 1000.0); + F->w_string(buffer); + } + + FS.w_close(F); + } +} + +/* + * Save reported data for sampling profiler. + * Saved data is represented by lines with folded stacks and count of samples. + * + * @param filename - target file to write report to + */ +void CScriptProfiler::saveSamplingReport(shared_str filename) +{ + if (m_sampling_profiling_log.empty()) + { + Msg("[P] Nothing to report for sampling profiler, data is missing"); + return; + } + + Msg("[P] Saving sampling report to %s", filename.c_str()); + IWriter* F = FS.w_open(filename.c_str()); + + if (F) + { + for (auto &it : m_sampling_profiling_log) + F->w_string(*it.getFoldedStack()); + + FS.w_close(F); + } +} + +/* + * @returns filename for hook profiler report + */ +shared_str CScriptProfiler::getHookReportFilename() +{ + string_path log_file_name; + strconcat(sizeof(log_file_name), log_file_name, Core.ApplicationName, "_", Core.UserName, "_hook_profile.log"); + FS.update_path(log_file_name, "$logs$", log_file_name); + + return log_file_name; +} + +/* + * @returns filename for sampling profiler report + */ +shared_str CScriptProfiler::getSamplingReportFilename() +{ + string_path log_file_name; + strconcat(sizeof(log_file_name), log_file_name, Core.ApplicationName, "_", Core.UserName, "_sampling_profile.perf"); + FS.update_path(log_file_name, "$logs$", log_file_name); + + return log_file_name; +} + +/* +* @returns whether profiling lua hook was/is attached to current VM context +*/ +bool CScriptProfiler::attachLuaHook() +{ + lua_Hook hook = lua_gethook(lua()); + + // Do not rewrite active hooks and verify if correct hooks is set. + // Avoid rewriting of hook since something else took hook place, preferrably we want only 1 hook in script engine. + if (hook) + return hook == CScriptEngine::lua_hook_call; + else + return lua_sethook(lua(), CScriptEngine::lua_hook_call, LUA_MASKLINE | LUA_MASKCALL | LUA_MASKRET, 0); +} + +/* + * Handle profiler dispose event. + * Make sure we are cleaning up all the data/links. + * + * @param L - lua VM active on dispose event + */ +void CScriptProfiler::onDispose(lua_State* L) +{ + // When handling instance disposal (reinit), stop profiling for VM. + // Otherwise you cannot stop profiling because VM pointer will be destroyed and become inaccessible. + if (m_active && m_profiler_type == CScriptProfilerType::Sampling) + { + Msg("[P] Disposing sampling profiler dependencies"); + luaJitProfilerStop(L); + } +} + +/* + * Handle engine reinit event within profiler. + * Required to switch current profiler context from one lua VM to newly initialize lua VM. + * + * @param L - new lua VM to initialize on + */ +void CScriptProfiler::onReinit(lua_State* L) +{ + if (!m_active) + return; + + Msg("[P] Profiler reinit, VM:%d", lua()); + + switch (m_profiler_type) + { + case CScriptProfilerType::Hook: + if (!attachLuaHook()) + { + Msg("[P] Cannot start scripts hook profiler on reinit, hook was not set properly"); + return; + } + + Msg("[P] Reinit scripts hook profiler"); + + return; + case CScriptProfilerType::Sampling: + { + if (!luaIsJitProfilerDefined()) + { + Msg("[P] Cannot start scripts sampling profiler on reinit, jit.profiler module is not defined"); + return; + } + + Msg("[P] Re-init scripts sampling profiler - attach handler, interval: %d", m_sampling_profile_interval); + luaJitSamplingProfilerAttach(this, m_sampling_profile_interval); + + return; + } + + default: NODEFAULT; + } +} + +/* + * Callback to handle lua profiling hook. + * Receiving lua events and collecting data in profiler instance to do further performance measurements and assumptions. + * + * @param L - lua VM context of calls + * @param dbg - lua debug context of hook call (only event data is valid) + */ +void CScriptProfiler::onLuaHookCall(lua_State* L, lua_Debug* dbg) +{ + if (!m_active || m_profiler_type != CScriptProfilerType::Hook || dbg->event == LUA_HOOKLINE) + return; + + auto [parent_stack_info, has_parent_stack_info] = luaDebugStackInfo(L, 2, "nSl"); + auto [stack_info, has_stack_info] = luaDebugStackInfo(L, 1, "nSl"); + auto [at_stack_info, has_at_stack_info] = luaDebugStackInfo(L, 0, "nSl"); + + if (!has_parent_stack_info || !has_stack_info) + return; + + string512 buffer; + + auto name = stack_info.name ? stack_info.name : at_stack_info.name ; + auto parent_name = parent_stack_info.name ? parent_stack_info.name : "[C]"; + auto short_src = stack_info.short_src; + auto parent_short_src = parent_stack_info.short_src; + auto line_defined = stack_info.linedefined; + auto parent_line_defined = parent_stack_info.linedefined; + + if (!name) + name = "?"; + + if (!stack_info.name && line_defined == 0) + name = "script-body"; + + if (xr_strcmp(short_src, parent_short_src) == 0) + xr_sprintf(buffer, "%s:%d;%s@%s:%d", name, line_defined, parent_name, parent_short_src, parent_line_defined); + else + xr_sprintf(buffer, "%s@%s:%d;%s@%s:%d", name, short_src, line_defined, parent_name, parent_short_src, + parent_line_defined); + + auto it = m_hook_profiling_portions.find(buffer); + bool exists = it != m_hook_profiling_portions.end(); + + switch (dbg->event) + { + case LUA_HOOKCALL: + { + if (exists) + it->second.start(); + else + { + CScriptProfilerHookPortion portion; + + portion.start(); + + m_hook_profiling_portions.emplace(buffer, std::move(portion)); + } + + return; + } + case LUA_HOOKRET: + case LUA_HOOKTAILRET: + { + if (exists) + it->second.stop(); + + return; + } + default: NODEFAULT; + } +} + +/* + * @returns reference to currently active lua VM state (or null if it was not initialize yet) + */ +lua_State* CScriptProfiler::lua() const +{ + return this->m_engine->lua(); +} + +/* + * @returns used memory by lua state in bytes + */ +int CScriptProfiler::luaMemoryUsed(lua_State* L) +{ + return lua_gc(L, LUA_GCCOUNT, 0) * 1024 + lua_gc(L, LUA_GCCOUNTB, 0); +} + +/* + * @returns whether jit is enabled + */ +bool CScriptProfiler::luaIsJitProfilerDefined() +{ + // Safest and least invasive way to check it. + // Other methods affect lua stack and may add interfere with VS extensions / other hooks / error callbacks. + // We assume that we do not load JIT libs only if nojit parameter is provided. + return !strstr(Core.Params, CScriptEngine::ARGUMENT_ENGINE_NOJIT); +} + +/* + * Attach sampling profiling hooks. + * With provided period report samples and store information in profiler for further reporting. + * + * @param interval - sampling interval for built-in luaJIT profiler +*/ +void CScriptProfiler::luaJitSamplingProfilerAttach(CScriptProfiler* profiler, u32 interval) +{ + string32 buffer = "fli"; + xr_itoa(interval, buffer + 3, 10); + + luaJitProfilerStart( + profiler->lua(), buffer, + [](void* data, lua_State* L, int samples, int vmstate) { + CScriptProfiler* profiler = static_cast(data); + + profiler->m_sampling_profiling_log.push_back(std::move(CScriptProfilerSamplingPortion( + luaJitProfilerDumpToString(L, "fl", 1), + luaJitProfilerDumpToString(L, "flZ;", -64), + samples, + vmstate, + luaMemoryUsed(L) + ))); + }, + profiler); +} + +/* + * Possible modes for profiling: + * f — profile with precision down to the function level + * l — profile with precision down to the line level + * i — sampling interval in milliseconds (default 10ms) + * + * Note: The actual sampling precision is OS-dependent. + * + * @param mode - jit profiling mode variant + * @param callback - callback to handle periodic sampling report event + * @param data - any pointer to receive in sampling report callback to return feedback + * @returns whether jit profiler start call was successful + */ +void CScriptProfiler::luaJitProfilerStart(lua_State* L, cpcstr mode, luaJIT_profile_callback callback, void* data) +{ + // Only single JIT profiler can exist and it will not attach with multiple states. + // Also only VM started profiler can end it, be careful. + luaJIT_profile_start(L, mode, callback, data); +} + +/* + * Stop JIT built-in sampling profiler. + * + * Notes: + * - failsafe, can stop already stopped data + * - profiler is singleton instance across all lua VM states + * - cannot stop profiler with VM reference, if it was started with another instance + * - no status / possibility to check if stop was successful without modifying luaJIT + */ +void CScriptProfiler::luaJitProfilerStop(lua_State* L) +{ + luaJIT_profile_stop(L); +} + +/* + * Possible format values for dump: + * f — dump function name + * F — dump function name, module:name for F variant + * p — preserve full path + * l — dump module:line + * Z — zap trailing separator + * i — Sampling interval in milliseconds (default 10ms) + * + * @returns jit profiler dump as shared string + */ +shared_str CScriptProfiler::luaJitProfilerDumpToString(lua_State* L, cpcstr format, int depth) +{ + string2048 buffer; + size_t length; + cpcstr dump = luaJIT_profile_dumpstack(L, format, depth, &length); + + R_ASSERT2(length < sizeof(buffer), "Profiling dump buffer overflow"); + strncpy_s(buffer, sizeof(buffer), dump, length); + buffer[length] = 0; + + return shared_str(buffer); +} + +/* + * Possible format values for dump: + * f — dump function name + * F — dump function name, module:name for F variant + * p — preserve full path + * l — dump module:line + * Z — zap trailing separator + * i — Sampling interval in milliseconds (default 10ms) + * + * @returns pair with dump char buffer and length of valid dump data in it + */ +std::pair CScriptProfiler::luaJitProfilerDump(lua_State* L, cpcstr format, int depth) +{ + size_t length; + cpcstr dump = luaJIT_profile_dumpstack(L, format, depth, &length); + + return std::make_pair(dump, length); +} + +/* + * @returns pair with debug information and status of debug information (whether was able to get info from stack) + */ +std::pair CScriptProfiler::luaDebugStackInfo(lua_State* L, int level, cpcstr what) +{ + lua_Debug info; + bool has_stack = lua_getstack(L, level, &info); + + if (has_stack) + { + lua_getinfo(L, what, &info); + } + + return std::make_pair(std::move(info), has_stack); +} diff --git a/src/xrScriptEngine/script_profiler.hpp b/src/xrScriptEngine/script_profiler.hpp new file mode 100644 index 00000000000..46086938087 --- /dev/null +++ b/src/xrScriptEngine/script_profiler.hpp @@ -0,0 +1,83 @@ +#pragma once + +#include "pch.hpp" +#include "script_profiler_portions.hpp" +#include "xrCommon/xr_unordered_map.h" + +struct lua_State; +struct lua_Debug; + +enum class CScriptProfilerType : u32 +{ + None = 0, + Hook = 1, + Sampling = 2, +}; + +class XRSCRIPTENGINE_API CScriptProfiler +{ +// todo: Can we make some global module to store all the arguments as experessions? +public: + // List of commnad line args for startup profiler attach: + constexpr static cpcstr ARGUMENT_PROFILER_DEFAULT = "-lua_profiler"; + constexpr static cpcstr ARGUMENT_PROFILER_HOOK = "-lua_hook_profiler"; + constexpr static cpcstr ARGUMENT_PROFILER_SAMPLING = "-lua_sampling_profiler"; + + static const CScriptProfilerType PROFILE_TYPE_DEFAULT = CScriptProfilerType::Hook; + static const u32 PROFILE_ENTRIES_LOG_LIMIT_DEFAULT = 128; + static const u32 PROFILE_SAMPLING_INTERVAL_DEFAULT = 10; + static const u32 PROFILE_SAMPLING_INTERVAL_MAX = 1000; + +private: + CScriptEngine* m_engine; + CScriptProfilerType m_profiler_type; + bool m_active; + + xr_unordered_map m_hook_profiling_portions; + xr_vector m_sampling_profiling_log; + /* + * Sampling interval for JIT based profiler. + * Value should be set in ms and defaults to 10ms. + */ + u32 m_sampling_profile_interval; + +public: + CScriptProfiler(CScriptEngine* engine); + virtual ~CScriptProfiler(); + + bool isActive() const { return m_active; }; + CScriptProfilerType getType() const { return m_profiler_type; }; + shared_str getTypeString() const; + u32 getRecordsCount() const; + + void start(CScriptProfilerType profiler_type = PROFILE_TYPE_DEFAULT); + void startHookMode(); + void startSamplingMode(u32 sampling_interval = PROFILE_SAMPLING_INTERVAL_DEFAULT); + void stop(); + void reset(); + void logReport(u32 entries_limit = PROFILE_ENTRIES_LOG_LIMIT_DEFAULT); + void logHookReport(u32 entries_limit = PROFILE_ENTRIES_LOG_LIMIT_DEFAULT); + void logSamplingReport(u32 entries_limit = PROFILE_ENTRIES_LOG_LIMIT_DEFAULT); + void saveReport(); + void saveHookReport(shared_str filename); + void saveSamplingReport(shared_str filename); + shared_str getHookReportFilename(); + shared_str getSamplingReportFilename(); + + bool attachLuaHook(); + void onReinit(lua_State* L); + void onDispose(lua_State* L); + void onLuaHookCall(lua_State* L, lua_Debug* dbg); + +private: + lua_State* lua() const; + + static int luaMemoryUsed(lua_State* L); + static bool luaIsJitProfilerDefined(); + static void luaJitSamplingProfilerAttach(CScriptProfiler* profiler, u32 interval); + static void luaJitProfilerStart(lua_State* L, cpcstr mode, luaJIT_profile_callback callback, void* data); + static void luaJitProfilerStop(lua_State* L); + static shared_str luaJitProfilerDumpToString(lua_State* L, cpcstr format, int depth); + static std::pair luaJitProfilerDump(lua_State* L, cpcstr format, int depth); + static std::pair luaDebugStackInfo(lua_State* L, int level, cpcstr what); +}; diff --git a/src/xrScriptEngine/script_profiler_portions.hpp b/src/xrScriptEngine/script_profiler_portions.hpp new file mode 100644 index 00000000000..956d925551c --- /dev/null +++ b/src/xrScriptEngine/script_profiler_portions.hpp @@ -0,0 +1,92 @@ +#pragma once +#include "pch.hpp" + +class CScriptProfilerHookPortion { + using Clock = std::chrono::high_resolution_clock; + using Time = Clock::time_point; + using Duration = Clock::duration; + + private: + u64 m_calls_count; + u32 m_calls_active; + + Time m_started_at; + Duration m_duration; + + public: + CScriptProfilerHookPortion(): m_calls_count(0), m_calls_active(0), m_duration(0), m_started_at() {} + + void start() + { + m_calls_count += 1; + + if (m_calls_active) + { + m_calls_active += 1; + return; + } + else + { + m_started_at = Clock::now(); + m_calls_active += 1; + } + } + + void stop() + { + if (!m_calls_active) + return; + + m_calls_active -= 1; + + if (m_calls_active) + return; + + const auto now = Clock::now(); + + if (now > m_started_at) + m_duration += now - m_started_at; + } + + u64 count() const { return m_calls_count; } + + u64 duration() const + { + using namespace std::chrono; + return u64(duration_cast(m_duration).count()); + } +}; + +class CScriptProfilerSamplingPortion { + using Clock = std::chrono::high_resolution_clock; + using Time = Clock::time_point; + + public: + Time m_recoreded_at; + + int m_memory; + int m_samples; + int m_state; + shared_str m_name; + shared_str m_trace; + + CScriptProfilerSamplingPortion(shared_str name, shared_str trace, int samples, int state, int memory) + : m_name(name), m_trace(trace), m_memory(memory), m_samples(samples), m_state(state), + m_recoreded_at(Clock::now()) {} + + CScriptProfilerSamplingPortion(const CScriptProfilerSamplingPortion& rhs) + : m_name(rhs.m_name), m_trace(rhs.m_trace), m_memory(rhs.m_memory), m_samples(rhs.m_samples), + m_state(rhs.m_state), m_recoreded_at(rhs.m_recoreded_at) {} + + CScriptProfilerSamplingPortion cloned() const { return CScriptProfilerSamplingPortion(*this); } + + // Build flamechart folded stack including frames and samples count + // Example: `C;frame_1_func:24;frame_2_func:45 4` + shared_str getFoldedStack() + { + string2048 buffer; + xr_sprintf(buffer, "%c;%s %d", m_state, m_trace.c_str(), m_samples); + + return shared_str(buffer); + } +}; diff --git a/src/xrScriptEngine/xrScriptEngine.hpp b/src/xrScriptEngine/xrScriptEngine.hpp index a1bdf1f5ee3..fc16f5fcb55 100644 --- a/src/xrScriptEngine/xrScriptEngine.hpp +++ b/src/xrScriptEngine/xrScriptEngine.hpp @@ -10,6 +10,7 @@ extern "C" { #include #include #include +#include } #pragma warning(disable : 4127) // conditional expression is constant diff --git a/src/xrScriptEngine/xrScriptEngine.vcxproj b/src/xrScriptEngine/xrScriptEngine.vcxproj index 2d2521c36e3..015c5d753fd 100644 --- a/src/xrScriptEngine/xrScriptEngine.vcxproj +++ b/src/xrScriptEngine/xrScriptEngine.vcxproj @@ -50,6 +50,8 @@ + + @@ -68,6 +70,7 @@ + diff --git a/src/xrScriptEngine/xrScriptEngine.vcxproj.filters b/src/xrScriptEngine/xrScriptEngine.vcxproj.filters index b774e488542..67e4aa3b5fd 100644 --- a/src/xrScriptEngine/xrScriptEngine.vcxproj.filters +++ b/src/xrScriptEngine/xrScriptEngine.vcxproj.filters @@ -45,6 +45,12 @@ Debug + + Debug + + + Debug + Debug @@ -143,6 +149,9 @@ Debug + + Debug + Process