Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Control command capture and replay widget state through UI #346

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
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
4 changes: 4 additions & 0 deletions LuaMenu/widgets/chobby/components/configuration.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
73 changes: 48 additions & 25 deletions LuaMenu/widgets/dbg_command_capture.lua
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
-- Sometimes this gets cached, hence the variable..
local ENABLED = false

function widget:GetInfo()
return {
name = "Command capture",
Expand All @@ -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 = {}
Expand All @@ -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
60 changes: 35 additions & 25 deletions LuaMenu/widgets/dbg_command_replay.lua
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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
2 changes: 2 additions & 0 deletions LuaMenu/widgets/gui_settings_window.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 9 additions & 6 deletions profile/parse_and_plot.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -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."
]
},
{
Expand All @@ -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\""
]
},
Expand All @@ -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",
Expand Down Expand Up @@ -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",
Expand Down