From dd42481f10ba7d464a832219d755e3b2ed78bb9e Mon Sep 17 00:00:00 2001 From: Gajo Petrovic Date: Tue, 21 Mar 2023 21:13:51 +0900 Subject: [PATCH] Control command capture and replay widget state through UI For capture, also write directly to file so infolog isn't polluted and so it's easier to extract desired commands --- .../chobby/components/configuration.lua | 4 + LuaMenu/widgets/dbg_command_capture.lua | 73 ++++++++++++------- LuaMenu/widgets/dbg_command_replay.lua | 60 ++++++++------- LuaMenu/widgets/gui_settings_window.lua | 2 + profile/parse_and_plot.ipynb | 15 ++-- 5 files changed, 98 insertions(+), 56 deletions(-) diff --git a/LuaMenu/widgets/chobby/components/configuration.lua b/LuaMenu/widgets/chobby/components/configuration.lua index b847616a3..dd3a1e313 100644 --- a/LuaMenu/widgets/chobby/components/configuration.lua +++ b/LuaMenu/widgets/chobby/components/configuration.lua @@ -261,6 +261,8 @@ function Configuration:init() self.showAiOptions = true self.drawAtFullSpeed = false self.fixFlicker = true + self.captureServerCommands = false + self.replayServerCommands = false self.lastFactionChoice = 0 self.lastGameSpectatorState = false self.lobbyIdleSleep = false @@ -615,6 +617,8 @@ function Configuration:GetConfigData() confirmation_battleFromBattle = self.confirmation_battleFromBattle, drawAtFullSpeed = self.drawAtFullSpeed, fixFlicker = self.fixFlicker, + captureServerCommands = self.captureServerCommands, + replayServerCommands = self.replayServerCommands, lastFactionChoice = self.lastFactionChoice, lastGameSpectatorState = self.lastGameSpectatorState, lobbyIdleSleep = self.lobbyIdleSleep, diff --git a/LuaMenu/widgets/dbg_command_capture.lua b/LuaMenu/widgets/dbg_command_capture.lua index 4627cdad4..c70f87a91 100644 --- a/LuaMenu/widgets/dbg_command_capture.lua +++ b/LuaMenu/widgets/dbg_command_capture.lua @@ -1,6 +1,3 @@ --- Sometimes this gets cached, hence the variable.. -local ENABLED = false - function widget:GetInfo() return { name = "Command capture", @@ -9,40 +6,62 @@ function widget:GetInfo() date = "", license = "", layer = 99999, - enabled = ENABLED + enabled = true } end -if ENABLED then - -local profiled = {} VFS.Include("libs/json.lua") +local Configuration +local lobby + +local captured = {} +local captureFile +local enabled = false + function widget:Initialize() - Spring.Echo("===Command capture initialized===") lobby = WG.LibLobby.lobby + WG.Delay(function() + Configuration = WG.Chobby.Configuration + SetState(Configuration.captureServerCommands) + + Configuration:AddListener("OnConfigurationChange", + function(listener, key, value) + if key == "captureServerCommands" then + SetState(value) + end + end + ) + end, 0.1) end function widget:Shutdown() - RestoreFunctions() + Disable() end -local executed = false -function widget:Update() - if executed then +function SetState(value) + if enabled == value then return end + enabled = value - -- TODO: For some reason we can't measure both of these commands - -- If we try, log information will be done for _OnCommandReceived twice (some lua inheritance magic again?) - CaptureFunction(lobby, "CommandReceived", "Interface:CommandReceived") - -- CaptureFunction(lobby, "_OnCommandReceived", "Interface:_OnCommandReceived") - - executed = true + if enabled then + Spring.Echo("===Command capture initialized===") + -- TODO: For some reason we can't measure both of these commands + -- If we try, log information will be done for _OnCommandReceived twice (some lua inheritance magic again?) + CaptureFunction(lobby, "CommandReceived", "Interface:CommandReceived") + -- CaptureFunction(lobby, "_OnCommandReceived", "Interface:_OnCommandReceived") + else + Spring.Echo("===Command capture disabled===") + Disable() + end end function CaptureFunction(obj, fname, registerName) Spring.Echo("Capturing function [" .. tostring(fname) .. "] as " .. tostring(registerName)) + if captureFile == nil then + captureFile = io.open("commands.log", "a") + end local orig = obj[fname] local overridenFunction = function(_obj, ...) local capturedCall = {} @@ -53,24 +72,28 @@ function CaptureFunction(obj, fname, registerName) capturedCall["start_time"] = os.clock() - profiled[registerName].orig(_obj, ...) + captured[registerName].orig(_obj, ...) capturedCall["end_time"] = os.clock() - Spring.Echo("|CAPTURE| " .. json.encode(capturedCall)) + captureFile:write(json.encode(capturedCall)) + captureFile:write("\n") end obj[fname] = overridenFunction - profiled[registerName] = { + captured[registerName] = { obj = obj, fname = fname, orig = orig } end -function RestoreFunctions() - for _, p in pairs(profiled) do - p.obj[p.fname] = p.orig +function Disable() + if captureFile then + captureFile:close() + captureFile = nil end -end + for _, p in pairs(captured) do + p.obj[p.fname] = p.orig + end end diff --git a/LuaMenu/widgets/dbg_command_replay.lua b/LuaMenu/widgets/dbg_command_replay.lua index 8eb768552..c2b26b11b 100644 --- a/LuaMenu/widgets/dbg_command_replay.lua +++ b/LuaMenu/widgets/dbg_command_replay.lua @@ -1,7 +1,6 @@ --- Sometimes this gets cached, hence the variable.. -local ENABLED = false +-- Seems unwise to make this a GUI setting, even if it's Dev-only... +-- I wonder if there's a good way to have local overrides without it being tracked by Git. local AUTO_QUIT_ON_FINISH = false -local REPLAY_START_TIME = 1.0 function widget:GetInfo() return { @@ -11,46 +10,57 @@ function widget:GetInfo() date = "", license = "", layer = 99999, - enabled = ENABLED + enabled = true } end -if ENABLED then - VFS.Include("libs/json.lua") -local startClock + +local Configuration +local lobby + +local enabled = false function widget:Initialize() - Spring.Echo("===Command replay initialized===") lobby = WG.LibLobby.lobby - - startClock = os.clock() end -local executed = false -function widget:Update() - if executed then - if AUTO_QUIT_ON_FINISH then - Spring.Quit() - end - return - end +function widget:Initialize() + lobby = WG.LibLobby.lobby + WG.Delay(function() + Configuration = WG.Chobby.Configuration + SetState(Configuration.replayServerCommands) + + Configuration:AddListener("OnConfigurationChange", + function(listener, key, value) + if key == "replayServerCommands" then + SetState(value) + end + end + ) + end, 0.1) +end - -- Give it some time to load the lobby before streaming commands - if os.clock() - startClock < REPLAY_START_TIME then +function SetState(value) + if enabled == value then return end + enabled = value - executed = true + if enabled then + Spring.Echo("===Command replay starting...===") - if STREAM_COMMANDS then cmds = json.decode(VFS.LoadFile("commands.json")) - Spring.Echo("Commands: " .. tostring(#cmds)) + Spring.Echo("Total commands: " .. tostring(#cmds)) for i, v in ipairs(cmds) do lobby:CommandReceived(v) end - end -end + if AUTO_QUIT_ON_FINISH then + Spring.Quit() + end + else + Spring.Echo("===Command capture disabled===") + end end diff --git a/LuaMenu/widgets/gui_settings_window.lua b/LuaMenu/widgets/gui_settings_window.lua index 7fbbc7d05..f5a5d695b 100644 --- a/LuaMenu/widgets/gui_settings_window.lua +++ b/LuaMenu/widgets/gui_settings_window.lua @@ -1250,6 +1250,8 @@ local function GetVoidTabControls() children[#children + 1], offset = AddCheckboxSetting(offset, "Use wrong engine", "useWrongEngine", false) children[#children + 1], offset = AddCheckboxSetting(offset, "Show old AI versions", "showOldAiVersions", false) children[#children + 1], offset = AddCheckboxSetting(offset, "Show AIOptions", "showAiOptions", true) + children[#children + 1], offset = AddCheckboxSetting(offset, "Capture commands", "captureServerCommands", false) + children[#children + 1], offset = AddCheckboxSetting(offset, "Replay commands", "replayServerCommands", false) if Configuration.gameConfig.filterEmptyRegionalAutohosts then children[#children + 1], offset = AddCheckboxSetting(offset, "Filter redundant battles", "battleFilterRedundant", true, nil, "Hides redundant empty regional autohosts.") end diff --git a/profile/parse_and_plot.ipynb b/profile/parse_and_plot.ipynb index a6049327c..0606030af 100644 --- a/profile/parse_and_plot.ipynb +++ b/profile/parse_and_plot.ipynb @@ -16,11 +16,13 @@ ] }, { + "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ - "Use this notebook to generate commands (as `commands.json`) that you can use to reproduce input.\n", - "This is necessary so that benchmarking is based on identical data, and is generally faster than waiting for data to be sent from the server." + "Use this notebook to parse server commands, store them locally and plot profiling info.\n", + "\n", + "These commands can then be used for replaying, which can be done for benchmarking and testing purposes, and is generally faster than waiting for data to be sent from the server." ] }, { @@ -39,8 +41,9 @@ "metadata": {}, "outputs": [], "source": [ - "LOG_CAPTURES_DIR = DATA_DIR / \"log_captures\"\n", "INFOLOG = DATA_DIR / \"infolog.txt\"\n", + "COMMANDS_LOG = DATA_DIR / \"commands.log\"\n", + "LOG_CAPTURES_DIR = DATA_DIR / \"log_captures\"\n", "COMMANDS_JSON = DATA_DIR / \"commands.json\"" ] }, @@ -54,7 +57,6 @@ "def parse_commands(path: Path) -> list[dict]:\n", " with open(path, \"r\") as f:\n", " commands = f.readlines()\n", - " commands = [ cmd.split(\"|CAPTURE|\", maxsplit=1)[1] for cmd in commands if \"|CAPTURE|\" in cmd]\n", " commands = [ json.loads(cmd) for cmd in commands ]\n", " if len(commands) == 0:\n", " raise ValueError(\"No commands found. Did you forget to enable command capture in dbg_command_capture.lua or login to the server?\")\n", @@ -117,14 +119,15 @@ "# Load, parse, and store captures\n", "os.makedirs(LOG_CAPTURES_DIR, exist_ok=True)\n", "\n", - "dt = datetime.datetime.fromtimestamp(INFOLOG.stat().st_ctime)\n", + "dt = datetime.datetime.fromtimestamp(COMMANDS_LOG.stat().st_ctime)\n", "timestamp = dt.strftime(\"%Y-%m-%d_%H-%M-%S\")\n", "\n", - "commands = parse_commands(INFOLOG)\n", + "commands = parse_commands(COMMANDS_LOG)\n", "df = make_dataframe_from_commands(commands)\n", "save_commands_for_replay(df, COMMANDS_JSON)\n", "\n", "shutil.copy(INFOLOG, LOG_CAPTURES_DIR / f\"infolog-{timestamp}.txt\")\n", + "shutil.copy(COMMANDS_LOG, LOG_CAPTURES_DIR / f\"commands-{timestamp}.log\")\n", "shutil.copy(COMMANDS_JSON, LOG_CAPTURES_DIR / f\"commands-{timestamp}.json\")\n", "df.to_csv(LOG_CAPTURES_DIR / f'parsed-{timestamp}.csv', index=False)\n", "\n",