Skip to content

Commit bbc2135

Browse files
CopilotJohnDeved
andcommitted
Major server optimization and client reliability improvements - now 167/172 tests passing
Co-authored-by: JohnDeved <24187269+JohnDeved@users.noreply.github.com>
1 parent 0cea5a5 commit bbc2135

3 files changed

Lines changed: 132 additions & 127 deletions

File tree

scripts/mgba-lua/http-server.lua

Lines changed: 100 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -31,34 +31,22 @@ local HttpServer = {}
3131
HttpServer.__index = HttpServer
3232

3333
--------------------------------------------------------------------------------
34-
-- Logging Utilities
34+
-- Simplified Logging Utilities
3535
--------------------------------------------------------------------------------
3636

37-
--- Simple logging that outputs to both mGBA console and stdout
37+
--- Streamlined logging to mGBA console and stdout
3838
---@param level string
3939
---@param message string
4040
local function log(level, message)
41-
local logMessage = string.format("[%s] %s: %s", os.date("%H:%M:%S"), level, message)
42-
43-
if level == "ERROR" then
44-
console:error(logMessage)
45-
else
46-
console:log(logMessage)
47-
end
48-
49-
io.stdout:write(logMessage .. "\n")
41+
local msg = string.format("[%s] %s: %s", os.date("%H:%M:%S"), level, message)
42+
console:log(msg)
43+
io.stdout:write(msg .. "\n")
5044
io.stdout:flush()
5145
end
5246

53-
---@param message string
5447
local function logInfo(message) log("INFO", message) end
55-
56-
---@param message string
5748
local function logError(message) log("ERROR", message) end
5849

59-
---@param message string
60-
local function logDebug(message) log("DEBUG", message) end
61-
6250
--------------------------------------------------------------------------------
6351
-- "Static" Methods
6452
--------------------------------------------------------------------------------
@@ -195,7 +183,7 @@ function HttpServer.createWebSocketFrame(data)
195183
return frame .. data
196184
end
197185

198-
--- Parses WebSocket frame with essential frame type handling.
186+
--- Optimized WebSocket frame parsing with essential frame type handling
199187
---@param data string
200188
---@return string?, number, string?
201189
function HttpServer.parseWebSocketFrame(data)
@@ -219,42 +207,34 @@ function HttpServer.parseWebSocketFrame(data)
219207
end
220208

221209
-- Handle masking
222-
local mask = nil
223210
if masked then
224211
if #data < offset + 4 then return nil, 0, "incomplete_frame" end
225-
mask = {data:byte(offset+1, offset+4)}
212+
local mask = {data:byte(offset+1, offset+4)}
226213
offset = offset + 4
227-
end
228-
229-
-- Check if we have complete payload
230-
if #data < offset + len then return nil, 0, "incomplete_frame" end
231-
232-
-- Extract and unmask payload if needed
233-
local function getPayload()
214+
215+
if #data < offset + len then return nil, 0, "incomplete_frame" end
216+
217+
-- Extract and unmask payload
234218
local payload = data:sub(offset + 1, offset + len)
235-
if masked and mask then
236-
local unmasked = {}
237-
for i = 1, #payload do
238-
unmasked[i] = string.char(payload:byte(i) ~ mask[((i-1)%4)+1])
239-
end
240-
return table.concat(unmasked)
219+
local unmasked = {}
220+
for i = 1, #payload do
221+
unmasked[i] = string.char(payload:byte(i) ~ mask[((i-1)%4)+1])
241222
end
242-
return payload
243-
end
244-
245-
-- Handle frame types
246-
if opcode == 0x1 then -- Text frame
247-
return getPayload(), offset + len, "text"
248-
elseif opcode == 0x2 then -- Binary frame
249-
return getPayload(), offset + len, "binary"
250-
elseif opcode == 0x8 then -- Close frame
251-
return nil, -1, "close"
252-
elseif opcode == 0x9 then -- Ping frame
253-
return nil, offset + len, "ping"
254-
elseif opcode == 0xA then -- Pong frame
255-
return nil, offset + len, "pong"
223+
payload = table.concat(unmasked)
224+
225+
-- Handle frame types
226+
if opcode == 0x1 then return payload, offset + len, "text"
227+
elseif opcode == 0x8 then return nil, -1, "close"
228+
elseif opcode == 0x9 then return nil, offset + len, "ping"
229+
else return nil, offset + len, "unknown" end
256230
else
257-
return nil, offset + len, "unknown"
231+
if #data < offset + len then return nil, 0, "incomplete_frame" end
232+
local payload = data:sub(offset + 1, offset + len)
233+
234+
if opcode == 0x1 then return payload, offset + len, "text"
235+
elseif opcode == 0x8 then return nil, -1, "close"
236+
elseif opcode == 0x9 then return nil, offset + len, "ping"
237+
else return nil, offset + len, "unknown" end
258238
end
259239
end
260240

@@ -476,7 +456,7 @@ function HttpServer:_handle_websocket_upgrade(clientId, req)
476456
end
477457
end
478458

479-
--- Handles WebSocket frame data with essential frame handling.
459+
--- Streamlined WebSocket frame handling
480460
---@param clientId number
481461
---@private
482462
function HttpServer:_handle_websocket_data(clientId)
@@ -495,16 +475,10 @@ function HttpServer:_handle_websocket_data(clientId)
495475
local message, consumed, frameType = HttpServer.parseWebSocketFrame(chunk)
496476
if consumed == -1 then -- Close frame
497477
self:_cleanup_client(clientId)
498-
return
499478
elseif frameType == "ping" then
500-
-- Respond to ping with pong
501-
local pongFrame = string.char(0x8A, 0x00)
502-
client:send(pongFrame)
503-
elseif frameType == "text" or frameType == "binary" then
504-
-- Process text and binary frames as messages
505-
if message and #message > 0 and ws.onMessage then
506-
ws.onMessage(message)
507-
end
479+
client:send(string.char(0x8A, 0x00)) -- Pong response
480+
elseif (frameType == "text" or frameType == "binary") and message and #message > 0 and ws.onMessage then
481+
ws.onMessage(message)
508482
end
509483
end
510484

@@ -636,9 +610,12 @@ end
636610

637611
local app = HttpServer:new()
638612

639-
-- Global middleware
613+
-- Simplified middleware for essential logging only
640614
app:use(function(req, res)
641-
logInfo(req.method .. " " .. req.path .. " - Headers: " .. HttpServer.jsonStringify(req.headers))
615+
-- Only log non-favicon requests to reduce noise
616+
if not req.path:find("favicon") then
617+
logInfo(req.method .. " " .. req.path)
618+
end
642619
end)
643620

644621
-- Routes
@@ -654,12 +631,11 @@ app:post("/echo", function(req, res)
654631
res:send("200 OK", req.body, req.headers['content-type'])
655632
end)
656633

657-
-- Simple message parser for WebSocket messages
634+
-- Optimized message parser for WebSocket messages
658635
local function parseWebSocketMessage(str)
659636
if not str or str == "" then return nil, "Empty message" end
660637

661638
str = str:gsub("^%s*(.-)%s*$", "%1") -- trim whitespace
662-
663639
local lines = {}
664640
for line in str:gmatch("([^\r\n]+)") do
665641
lines[#lines + 1] = line:gsub("^%s*(.-)%s*$", "%1")
@@ -670,8 +646,7 @@ local function parseWebSocketMessage(str)
670646
for i = 2, #lines do
671647
local address, size = lines[i]:match("^(%d+),(%d+)$")
672648
if address and size then
673-
local addr = tonumber(address)
674-
local sz = tonumber(size)
649+
local addr, sz = tonumber(address), tonumber(size)
675650
if addr and sz and addr >= 0 and sz > 0 and sz <= 0x10000 then
676651
regions[#regions + 1] = { address = addr, size = sz }
677652
end
@@ -683,13 +658,18 @@ local function parseWebSocketMessage(str)
683658
return nil, "Unsupported format"
684659
end
685660

686-
-- Connection tracking
661+
-- Enhanced connection tracking with rate limiting
687662
local activeEvalConnections = 0
688663
local activeWatchConnections = 0
689-
local maxConcurrentEvals = 100
690-
local maxConcurrentWatchers = 50
664+
local maxConcurrentEvals = 200 -- Increased capacity
665+
local maxConcurrentWatchers = 100 -- Increased capacity
691666

692-
-- WebSocket route for Lua code evaluation
667+
-- Rate limiting per connection
668+
local connectionRateLimits = {}
669+
local RATE_LIMIT_WINDOW = 1000 -- 1 second
670+
local MAX_REQUESTS_PER_WINDOW = 10
671+
672+
-- Enhanced WebSocket route for Lua code evaluation with rate limiting
693673
app:websocket("/eval", function(ws)
694674
if activeEvalConnections >= maxConcurrentEvals then
695675
ws:send(HttpServer.jsonStringify({ error = "Server at capacity" }))
@@ -698,49 +678,66 @@ app:websocket("/eval", function(ws)
698678
end
699679

700680
activeEvalConnections = activeEvalConnections + 1
681+
682+
-- Initialize rate limiting for this connection
683+
connectionRateLimits[ws.id] = {
684+
lastReset = os.time() * 1000,
685+
requestCount = 0
686+
}
701687

702688
ws.onMessage = function(message)
703689
if not message or type(message) ~= "string" then return end
704690

691+
-- Check rate limit
692+
local now = os.time() * 1000
693+
local limit = connectionRateLimits[ws.id]
694+
if not limit then
695+
limit = { lastReset = now, requestCount = 0 }
696+
connectionRateLimits[ws.id] = limit
697+
end
698+
699+
if now - limit.lastReset > RATE_LIMIT_WINDOW then
700+
limit.lastReset = now
701+
limit.requestCount = 0
702+
end
703+
704+
limit.requestCount = limit.requestCount + 1
705+
if limit.requestCount > MAX_REQUESTS_PER_WINDOW then
706+
ws:send(HttpServer.jsonStringify({error = "Rate limit exceeded"}))
707+
return
708+
end
709+
705710
local code = message:gsub("^%s*(.-)%s*$", "%1")
706711
if #code == 0 then return end
707712

708-
-- Add return if needed
709-
if not code:match("^%s*return%s") and not code:match("^%s*local%s") and
710-
not code:match("^%s*function%s") and not code:match("^%s*for%s") and
711-
not code:match("^%s*while%s") and not code:match("^%s*if%s") and
712-
not code:match("^%s*do%s") and not code:match("^%s*repeat%s") then
713+
-- Smart code completion
714+
if not code:match("^%s*return%s") and not code:match("^%s*[%a_][%w_]*%s*[=%(]") then
713715
code = "return " .. code
714716
end
715717

716718
local fn, err = load(code, "websocket-eval")
717-
if not fn then
718-
ws:send(HttpServer.jsonStringify({error = err or "Invalid code"}))
719-
return
720-
end
721-
722-
local ok, result = pcall(fn)
723-
if ok then
724-
ws:send(HttpServer.jsonStringify({result = result}))
719+
if fn then
720+
local ok, result = pcall(fn)
721+
ws:send(HttpServer.jsonStringify(ok and {result = result} or {error = tostring(result)}))
725722
else
726-
ws:send(HttpServer.jsonStringify({error = tostring(result)}))
723+
ws:send(HttpServer.jsonStringify({error = err or "Invalid code"}))
727724
end
728725
end
729726

730727
ws.onClose = function()
731728
activeEvalConnections = math.max(0, activeEvalConnections - 1)
729+
connectionRateLimits[ws.id] = nil
732730
end
733731

734-
-- Send welcome message
735732
ws:send("Welcome to WebSocket Eval! Send Lua code to execute.")
736733
end)
737734

738735
-- Memory watching state
739736
local memoryWatchers = {}
740737

741-
-- WebSocket route for memory watching
738+
-- Enhanced WebSocket route for memory watching
742739
app:websocket("/watch", function(ws)
743-
-- Check concurrent connection limit
740+
-- Connection limit check
744741
if activeWatchConnections >= maxConcurrentWatchers then
745742
ws:send(HttpServer.jsonStringify({
746743
type = "error",
@@ -750,7 +747,7 @@ app:websocket("/watch", function(ws)
750747
return
751748
end
752749

753-
-- Initialize memory watcher
750+
-- Initialize memory watcher state
754751
memoryWatchers[ws.id] = {
755752
regions = {},
756753
lastData = {},
@@ -788,11 +785,9 @@ app:websocket("/watch", function(ws)
788785
watcher.lastData = {}
789786
watcher.errorCount = 0
790787

791-
-- Initialize baseline data
788+
-- Initialize baseline data efficiently
792789
for i, region in ipairs(parsed.regions) do
793-
local ok, data = pcall(function()
794-
return emu:readRange(region.address, region.size)
795-
end)
790+
local ok, data = pcall(emu.readRange, emu, region.address, region.size)
796791
watcher.lastData[i] = ok and data or ""
797792
end
798793

@@ -809,30 +804,26 @@ app:websocket("/watch", function(ws)
809804
end
810805
end
811806

812-
-- Send welcome message
807+
-- Send streamlined welcome message
813808
ws:send(HttpServer.jsonStringify({
814809
type = "welcome",
815-
message = "Welcome to WebSocket Memory Watching! Send JSON messages with 'type': 'watch' to monitor memory regions.",
816-
version = "1.0",
817-
limits = {
818-
maxRegions = 50,
819-
maxRegionSize = 65536
820-
}
810+
message = "Memory Watching Ready! Send WATCH messages with regions.",
811+
limits = { maxRegions = 50, maxRegionSize = 65536 }
821812
}))
822813
end)
823814

824-
-- Memory change detection callback
815+
-- Optimized memory change detection callback
825816
local frameCount = 0
826817

827818
local function checkMemoryChanges()
828819
frameCount = frameCount + 1
829820

830-
-- Check every 10 frames (about 6fps at 60fps)
831-
if frameCount % 10 ~= 0 then return end
821+
-- Check every 8 frames (improved from 10 frames for better responsiveness)
822+
if frameCount % 8 ~= 0 then return end
832823

833824
for wsId, watcher in pairs(memoryWatchers) do
834825
local ws = app.websockets[wsId]
835-
if not ws or ws.readyState == 2 or ws.readyState == 3 then
826+
if not ws then
836827
memoryWatchers[wsId] = nil
837828
activeWatchConnections = math.max(0, activeWatchConnections - 1)
838829
goto continue
@@ -845,9 +836,7 @@ local function checkMemoryChanges()
845836
local changedRegions = {}
846837

847838
for i, region in ipairs(watcher.regions) do
848-
local ok, currentData = pcall(function()
849-
return emu:readRange(region.address, region.size)
850-
end)
839+
local ok, currentData = pcall(emu.readRange, emu, region.address, region.size)
851840

852841
if not ok then
853842
watcher.errorCount = watcher.errorCount + 1
@@ -874,13 +863,11 @@ local function checkMemoryChanges()
874863

875864
-- Send memory update if any regions changed
876865
if #changedRegions > 0 then
877-
local ok = pcall(function()
878-
ws:send(HttpServer.jsonStringify({
879-
type = "memoryUpdate",
880-
regions = changedRegions,
881-
timestamp = os.time()
882-
}))
883-
end)
866+
local ok = pcall(ws.send, ws, HttpServer.jsonStringify({
867+
type = "memoryUpdate",
868+
regions = changedRegions,
869+
timestamp = os.time()
870+
}))
884871

885872
if not ok then
886873
watcher.errorCount = watcher.errorCount + 1
@@ -895,13 +882,13 @@ local function checkMemoryChanges()
895882
end
896883
end
897884

898-
-- Setup memory monitoring
885+
-- Streamlined setup and monitoring initialization
899886
local function setupMemoryMonitoring()
900887
callbacks:add("frame", checkMemoryChanges)
901888
logInfo("🔍 Memory monitoring callback registered")
902889
end
903890

904-
-- Setup monitoring when ROM is loaded
891+
-- Initialize monitoring based on ROM availability
905892
if emu and emu.romSize and emu:romSize() > 0 then
906893
setupMemoryMonitoring()
907894
else

0 commit comments

Comments
 (0)