fix(botapi): static SVG + intent buffers to kill cross-thread heap realloc
Round N+1's fresh bot thread realloc'd module-level strings/seqs (SVG buffer, intent stdout/stderr, team messages) left behind by dead round N's thread — same rawDealloc SIGSEGV class as the event queue, seen at graphics.nim:274 (drawText->prepareAdd, core 2490478 @ 03:13:40, battle round 257). - graphics.nim: gSvgBuffer -> array[16384, char] + gSvgLen, appendSvg - bot.nim: intent stdout/stderr -> static char arrays; team messages -> array[16, TeamMessage] + len; buildIntentJson/printToStdOut/Err/ broadcastTeamMessage bounded appends - botThreadEntry: reset graphics+intent buffers on the owning thread Also fixes stale mapActions call sites in tests/ (missing enemyX/enemyY).
This commit is contained in:
@@ -198,9 +198,21 @@ var gIntentGunColor: Color = Color(0)
|
||||
var gIntentAdjGunBody: bool = false
|
||||
var gIntentAdjRadarBody: bool = false
|
||||
var gIntentAdjRadarGun: bool = false
|
||||
var gIntentTeamMessages: seq[TeamMessage] = @[]
|
||||
var gIntentStdOut: string = ""
|
||||
var gIntentStdErr: string = ""
|
||||
# ponytail: static buffers instead of strings/seq. go()'s stop path returns
|
||||
# before buildIntentJson clears these, so a round's last tick can leave heap
|
||||
# blocks owned by the exiting bot thread -> the fresh next-round thread
|
||||
# reallocs a dead allocator block (rawDealloc SIGSEGV). Static storage:
|
||||
# no heap block crosses threads.
|
||||
const INTENT_STDOUT_CAP = 4096
|
||||
const INTENT_STDERR_CAP = 4096
|
||||
const INTENT_MSG_CAP = 16
|
||||
|
||||
var gIntentTeamMessages: array[INTENT_MSG_CAP, TeamMessage]
|
||||
var gIntentTeamMsgsLen: int
|
||||
var gIntentStdOut: array[INTENT_STDOUT_CAP, char]
|
||||
var gIntentStdOutLen: int
|
||||
var gIntentStdErr: array[INTENT_STDERR_CAP, char]
|
||||
var gIntentStdErrLen: int
|
||||
|
||||
proc buildIntentJson*(): string =
|
||||
## Serialise current intent to JSON for sending to server.
|
||||
@@ -237,9 +249,10 @@ proc buildIntentJson*(): string =
|
||||
obj["tracksColor"] = %gIntentTracksColor.toHex
|
||||
if gIntentGunColor != Color(0):
|
||||
obj["gunColor"] = %gIntentGunColor.toHex
|
||||
if gIntentTeamMessages.len > 0:
|
||||
if gIntentTeamMsgsLen > 0:
|
||||
var msgs = newJArray()
|
||||
for m in gIntentTeamMessages:
|
||||
for i in 0 ..< gIntentTeamMsgsLen:
|
||||
let m = gIntentTeamMessages[i]
|
||||
var mo = newJObject()
|
||||
mo["message"] = %m.message
|
||||
mo["messageType"] = %m.messageType
|
||||
@@ -247,13 +260,14 @@ proc buildIntentJson*(): string =
|
||||
mo["receiverId"] = %m.receiverId
|
||||
msgs.add mo
|
||||
obj["teamMessages"] = msgs
|
||||
gIntentTeamMessages.setLen 0
|
||||
if gIntentStdOut.len > 0:
|
||||
obj["stdOut"] = %gIntentStdOut
|
||||
gIntentStdOut = ""
|
||||
if gIntentStdErr.len > 0:
|
||||
obj["stdErr"] = %gIntentStdErr
|
||||
gIntentStdErr = ""
|
||||
for i in 0 ..< gIntentTeamMsgsLen: gIntentTeamMessages[i].reset
|
||||
gIntentTeamMsgsLen = 0
|
||||
if gIntentStdOutLen > 0:
|
||||
obj["stdOut"] = %($gIntentStdOut[0 ..< gIntentStdOutLen])
|
||||
gIntentStdOutLen = 0
|
||||
if gIntentStdErrLen > 0:
|
||||
obj["stdErr"] = %($gIntentStdErr[0 ..< gIntentStdErrLen])
|
||||
gIntentStdErrLen = 0
|
||||
let svg = svgOutput()
|
||||
if svg.len > 0:
|
||||
obj["debugGraphics"] = %svg
|
||||
@@ -311,19 +325,27 @@ proc setGunColor*(color: Color) = gIntentGunColor = color
|
||||
|
||||
proc printToStdOut*(s: string) =
|
||||
## Append s to this tick's stdOut payload (sent to server in BotIntent).
|
||||
gIntentStdOut.add s
|
||||
let n = min(s.len, INTENT_STDOUT_CAP - gIntentStdOutLen)
|
||||
for i in 0 ..< n: gIntentStdOut[gIntentStdOutLen + i] = s[i]
|
||||
inc gIntentStdOutLen, n
|
||||
|
||||
proc printToStdErr*(s: string) =
|
||||
## Append s to this tick's stdErr payload (sent to server in BotIntent).
|
||||
gIntentStdErr.add s
|
||||
let n = min(s.len, INTENT_STDERR_CAP - gIntentStdErrLen)
|
||||
for i in 0 ..< n: gIntentStdErr[gIntentStdErrLen + i] = s[i]
|
||||
inc gIntentStdErrLen, n
|
||||
|
||||
proc broadcastTeamMessage*(message: string) =
|
||||
## Send a message to all teammates this tick.
|
||||
gIntentTeamMessages.add TeamMessage(message: message, messageType: "String")
|
||||
if gIntentTeamMsgsLen < INTENT_MSG_CAP:
|
||||
gIntentTeamMessages[gIntentTeamMsgsLen] = TeamMessage(message: message, messageType: "String")
|
||||
inc gIntentTeamMsgsLen
|
||||
|
||||
proc sendTeamMessage*(botId: int; message: string) =
|
||||
## Send a message to a specific teammate this tick.
|
||||
gIntentTeamMessages.add TeamMessage(message: message, messageType: "String", receiverId: botId)
|
||||
if gIntentTeamMsgsLen < INTENT_MSG_CAP:
|
||||
gIntentTeamMessages[gIntentTeamMsgsLen] = TeamMessage(message: message, messageType: "String", receiverId: botId)
|
||||
inc gIntentTeamMsgsLen
|
||||
|
||||
proc setAdjustGunForBodyTurn*(v: bool) = gIntentAdjGunBody = v
|
||||
proc setAdjustRadarForBodyTurn*(v: bool) = gIntentAdjRadarBody = v
|
||||
@@ -808,6 +830,16 @@ proc botThreadEntry() {.thread.} =
|
||||
" prevDir=" & $gPreviousDirection &
|
||||
" prevGunDir=" & $gPreviousGunDirection)
|
||||
|
||||
# Reset graphics + intent buffers on the thread that owns them. go()'s
|
||||
# stop path (round end) returns before buildIntentJson/clearGraphics, so
|
||||
# the previous round's thread can leave content behind; resetting here
|
||||
# keeps it from leaking into this round's first intent.
|
||||
clearGraphics()
|
||||
gIntentStdOutLen = 0
|
||||
gIntentStdErrLen = 0
|
||||
for i in 0 ..< gIntentTeamMsgsLen: gIntentTeamMessages[i].reset
|
||||
gIntentTeamMsgsLen = 0
|
||||
|
||||
dispatchPendingEvents(gBot) # dispatch events embedded in the first tick
|
||||
|
||||
try:
|
||||
|
||||
@@ -9,13 +9,29 @@ import ./color
|
||||
# Module-level state (single bot per process)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
var gSvgBuffer: string
|
||||
# ponytail: static char array + length instead of a heap string. A fresh bot
|
||||
# thread runs each round; a module-level string grown by thread N and cleared
|
||||
# ("") by thread N+1 free/reallocs a dead thread's allocator block ->
|
||||
# rawDealloc SIGSEGV (same crash class as the event queue seq; gdb-confirmed
|
||||
# in drawText mid-campaign). Static storage: no heap block crosses threads.
|
||||
const SVG_BUFFER_CAP = 16384
|
||||
|
||||
var gSvgLen: int
|
||||
var gSvgBuffer: array[SVG_BUFFER_CAP, char]
|
||||
var gStrokeColor: Color = WHITE
|
||||
var gFillColor: Color = WHITE
|
||||
var gStrokeWidth: float = 1.0
|
||||
var gFontFamily: string = "Arial"
|
||||
var gFontFamily: string = "Arial" # never rebound at runtime (setFont unused)
|
||||
var gFontSize: float = 12.0
|
||||
|
||||
proc appendSvg(s: string) =
|
||||
## Append an SVG fragment, dropping anything past the static cap.
|
||||
let room = SVG_BUFFER_CAP - gSvgLen
|
||||
if room > 0:
|
||||
let n = min(room, s.len)
|
||||
for i in 0 ..< n: gSvgBuffer[gSvgLen + i] = s[i]
|
||||
inc gSvgLen, n
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Internal helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -26,12 +42,12 @@ proc svgAttrs(): string =
|
||||
|
||||
proc svgOutput*(): string =
|
||||
## Returns the SVG fragment for this tick, or "" if nothing was drawn.
|
||||
if gSvgBuffer.len == 0: return ""
|
||||
"<g>" & gSvgBuffer & "</g>"
|
||||
if gSvgLen == 0: return ""
|
||||
"<g>" & $gSvgBuffer[0 ..< gSvgLen] & "</g>"
|
||||
|
||||
proc clearGraphics*() =
|
||||
## Reset buffer and all style globals to defaults. Called after each tick.
|
||||
gSvgBuffer = ""
|
||||
gSvgLen = 0
|
||||
gStrokeColor = WHITE
|
||||
gFillColor = WHITE
|
||||
gStrokeWidth = 1.0
|
||||
@@ -54,26 +70,26 @@ proc setFont*(family: string; size: float) =
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
proc drawLine*(x1, y1, x2, y2: float) =
|
||||
gSvgBuffer.add &"<line x1=\"{x1}\" y1=\"{y1}\" x2=\"{x2}\" y2=\"{y2}\" {svgAttrs()}/>"
|
||||
appendSvg(&"<line x1=\"{x1}\" y1=\"{y1}\" x2=\"{x2}\" y2=\"{y2}\" {svgAttrs()}/>")
|
||||
|
||||
proc drawRectangle*(x, y, w, h: float) =
|
||||
let attrs = &"stroke=\"{gStrokeColor.toHex}\" fill=\"none\" stroke-width=\"{gStrokeWidth}\""
|
||||
gSvgBuffer.add &"<rect x=\"{x}\" y=\"{y}\" width=\"{w}\" height=\"{h}\" {attrs}/>"
|
||||
appendSvg(&"<rect x=\"{x}\" y=\"{y}\" width=\"{w}\" height=\"{h}\" {attrs}/>")
|
||||
|
||||
proc fillRectangle*(x, y, w, h: float) =
|
||||
let attrs = &"stroke=\"none\" fill=\"{gFillColor.toHex}\""
|
||||
gSvgBuffer.add &"<rect x=\"{x}\" y=\"{y}\" width=\"{w}\" height=\"{h}\" {attrs}/>"
|
||||
appendSvg(&"<rect x=\"{x}\" y=\"{y}\" width=\"{w}\" height=\"{h}\" {attrs}/>")
|
||||
|
||||
proc drawCircle*(x, y, r: float) =
|
||||
let attrs = &"stroke=\"{gStrokeColor.toHex}\" fill=\"none\" stroke-width=\"{gStrokeWidth}\""
|
||||
gSvgBuffer.add &"<circle cx=\"{x}\" cy=\"{y}\" r=\"{r}\" {attrs}/>"
|
||||
appendSvg(&"<circle cx=\"{x}\" cy=\"{y}\" r=\"{r}\" {attrs}/>")
|
||||
|
||||
proc fillCircle*(x, y, r: float) =
|
||||
let attrs = &"stroke=\"none\" fill=\"{gFillColor.toHex}\""
|
||||
gSvgBuffer.add &"<circle cx=\"{x}\" cy=\"{y}\" r=\"{r}\" {attrs}/>"
|
||||
appendSvg(&"<circle cx=\"{x}\" cy=\"{y}\" r=\"{r}\" {attrs}/>")
|
||||
|
||||
proc drawText*(text: string; x, y: float) =
|
||||
gSvgBuffer.add &"<text x=\"{x}\" y=\"{y}\" font-family=\"{gFontFamily}\" font-size=\"{gFontSize}\">{text}</text>"
|
||||
appendSvg(&"<text x=\"{x}\" y=\"{y}\" font-family=\"{gFontFamily}\" font-size=\"{gFontSize}\">{text}</text>")
|
||||
|
||||
proc drawPolygon*(points: seq[(float, float)]) =
|
||||
var pts = ""
|
||||
@@ -81,7 +97,7 @@ proc drawPolygon*(points: seq[(float, float)]) =
|
||||
if pts.len > 0: pts.add ' '
|
||||
pts.add &"{px},{py}"
|
||||
let attrs = &"stroke=\"{gStrokeColor.toHex}\" fill=\"none\" stroke-width=\"{gStrokeWidth}\""
|
||||
gSvgBuffer.add &"<polygon points=\"{pts}\" {attrs}/>"
|
||||
appendSvg(&"<polygon points=\"{pts}\" {attrs}/>")
|
||||
|
||||
proc fillPolygon*(points: seq[(float, float)]) =
|
||||
var pts = ""
|
||||
@@ -89,4 +105,4 @@ proc fillPolygon*(points: seq[(float, float)]) =
|
||||
if pts.len > 0: pts.add ' '
|
||||
pts.add &"{px},{py}"
|
||||
let attrs = &"stroke=\"none\" fill=\"{gFillColor.toHex}\""
|
||||
gSvgBuffer.add &"<polygon points=\"{pts}\" {attrs}/>"
|
||||
appendSvg(&"<polygon points=\"{pts}\" {attrs}/>")
|
||||
|
||||
Reference in New Issue
Block a user