#!/usr/bin/env bash # Materialise the Suramadu config, the Jetty properties and MegaMek's client # settings for this match. # # All three are rendered at launch rather than baked into the image because each # carries something the image cannot know: the player name that the Swing client # logs in as, and the ports. Everything else in them is fixed and lives in # config/*.template under review. # # MegaMek rewrites clientsettings.xml on exit, so seeding it once at image build # would not survive; it is re-seeded per launch. set -euo pipefail # shellcheck disable=SC2034 # read by common.sh's log helpers ARENA_LOG_TAG=init/config # shellcheck source=../lib/common.sh . "${ARENA_HOME:-/opt/arena}/container/lib/common.sh" # shellcheck source=/dev/null . "$ARENA_RUN/players.env" MM_HOME="${MM_HOME:-$ARENA_HOME/megamek}" JAVA="${JAVA:-$ARENA_HOME/jre/bin/java}" CLIENT_HEAP="${ARENA_CLIENT_HEAP:-2G}" # The paint dispatcher tick. A change waits for the next one, so it is worth # about a tick and a half of latency per repaint - the largest server-side term # in a warm input-to-paint once the board has been sent. 33 was upstream's, 16 # is measured better (see PERFORMANCE.md); this makes going lower an experiment # rather than a rebuild. DRAW_DELAY_MS="${ARENA_DRAW_DELAY_MS:-16}" # Anything else to put on the client JVM, for an experiment that would # otherwise want a rebuild to change one system property. Appended last so it # can override what is set above. Empty in every ordinary match. CLIENT_VMARGS="${ARENA_CLIENT_VMARGS:-}" case "$DRAW_DELAY_MS" in ''|*[!0-9]*) die "ARENA_DRAW_DELAY_MS must be a whole number of milliseconds, got '$DRAW_DELAY_MS'" ;; esac mkdir -p "$ARENA_RUN/suramadu" # Served by Suramadu in place of the war's own index.html; see # suramadu/web/ in the repo. # # Copied into the run directory rather than served from the image, because one # file in it is written while the match is on: container/watch/results.sh drops # the result there the instant the game is decided, and suramadu/web/report.js # fetches it from the page. The image tree is owned by root and this runs as # uid 10001, so the copy is what makes that directory writable at all. # # The alternative was for the page to wait on the result reaching S3 and # headquarters serving it back, which is a good five seconds of network for a # document that is already sitting in this container. WEB_FOLDER="${WEB_FOLDER:-$ARENA_WEB}" if [ "$WEB_FOLDER" = "$ARENA_WEB" ]; then if [ -d "$ARENA_HOME/web" ]; then rm -rf "$ARENA_WEB" cp -r "$ARENA_HOME/web" "$ARENA_WEB" log "web folder staged at $ARENA_WEB ($(find "$ARENA_WEB" -type f | wc -l) files)" else # A checkout that has not assembled an image tree. The war's own page is # served instead, which loses the theme and the report - but a config that # points at a directory with no index.html in it serves nothing at all. warn "no web folder at $ARENA_HOME/web; serving the war's own page" WEB_FOLDER="$ARENA_HOME/web" fi fi # --- the profiling agent, if this match is being profiled --------------------- # The client JVM is the one Suramadu spawns, so the only way to put a flag on it # is through vmArgs here. Empty unless ARENA_PROFILE is set to something other # than 0, and it substitutes to nothing, so an unprofiled match renders the # config it rendered before this existed. # # Wall clock, not CPU: the question the client JVM has to answer is where the # EDT's *latency* goes, and a blocked EDT costs a player exactly as much as a # busy one. CPU time on the host JVM is a different question; see # container/entrypoint.sh. # # DebugNonSafepoints makes the JIT keep debug info at non-safepoint PCs, without # which inlined frames are attributed to whatever method they were inlined into # - the difference between "paintComponent" and "PNG encode inside # paintComponent", which is the whole question. # # The lazy-png-agent javaagent defers Suramadu's ImageConst PNG encode to first # use of toMessage() rather than doing it unconditionally in the constructor. # See instrument/lazy-png-agent/. # # On by default now, and it was off for a while: it rewrites a third-party class # at classload, on the path that encodes every frame a player sees, and nothing # had measured it under play. The A/B that earns it back has been run - same # build, same scenario, one human seat, only ARENA_LAZY_PNG differing: # # png/deflate samples on the EDT 737 -> 0 # EDT samples running 1648 -> 740 # first interaction of a match, p50 361ms -> 49ms # # The encode does not move off the thread, it stops happening: the constructor # encoded every image unconditionally and DrawConstantPool then threw the result # away whenever the hash already had one. Deferring to toMessage() means a cache # hit never encodes at all, and with ddImageCacheSize at 16384 nearly every # image is a hit. That also removes most of a match's warm-up cliff, which is # the part of the latency a player meets first. # # ARENA_LAZY_PNG=0 turns it off, for bisecting a rendering fault against stock # Suramadu behaviour. The Dockerfile runs HarnessMain against the real # ImageConst every build, so "it still applies and still defers" is checked # rather than assumed. LAZY_PNG="${ARENA_LAZY_PNG:-1}" PROFILE_VMARGS="" if [ "$LAZY_PNG" != "0" ]; then LAZY_PNG_AGENT="$ARENA_HOME/instrument/lazy-png-agent.jar" [ -r "$LAZY_PNG_AGENT" ] || die "lazy-png-agent.jar missing at $LAZY_PNG_AGENT (ARENA_LAZY_PNG=0 runs without it)" PROFILE_VMARGS=" -javaagent:$LAZY_PNG_AGENT" log "lazy-png-agent on: ImageConst PNG encode deferred to toMessage()" else log "lazy-png-agent off: ImageConst encodes in its constructor, as upstream does" fi if [ -n "${ARENA_PROFILE:-}" ] && [ "$ARENA_PROFILE" != "0" ]; then AP_SO="$ARENA_HOME/async-profiler/lib/libasyncProfiler.so" AP_EVENT="${ARENA_PROFILE_CLIENT_EVENT:-wall}" AP_INTERVAL="${ARENA_PROFILE_INTERVAL:-10ms}" AP_OUT="$ARENA_STATE/profile-client.collapsed" # A missing agent would make the client JVM refuse to start at all, which # reads as "the match never loaded". Say why here instead. [ -r "$AP_SO" ] || die "ARENA_PROFILE is set but $AP_SO is missing" PROFILE_VMARGS="$PROFILE_VMARGS -XX:+UnlockDiagnosticVMOptions -XX:+DebugNonSafepoints" PROFILE_VMARGS="$PROFILE_VMARGS -agentpath:$AP_SO=start,event=$AP_EVENT,interval=$AP_INTERVAL,memlimit=128m,collapsed,file=$AP_OUT" log "profiling the client JVM: event=$AP_EVENT interval=$AP_INTERVAL -> $AP_OUT" fi [ -n "$CLIENT_VMARGS" ] && PROFILE_VMARGS="$PROFILE_VMARGS $CLIENT_VMARGS" \ && log "extra client vmArgs: $CLIENT_VMARGS" sed -e "s|@PROFILE_VMARGS@|$PROFILE_VMARGS|g" \ -e "s|@WEB_FOLDER@|$WEB_FOLDER|g" \ -e "s|@ARENA_HOME@|$ARENA_HOME|g" \ -e "s|@MM_HOME@|$MM_HOME|g" \ -e "s|@JAVA@|$JAVA|g" \ -e "s|@PORT_MM@|$PORT_MM|g" \ -e "s|@PLAYER_NAME@|$ARENA_CLIENT_SLOT|g" \ -e "s|@CLIENT_HEAP@|$CLIENT_HEAP|g" \ -e "s|@DRAW_DELAY_MS@|$DRAW_DELAY_MS|g" \ "$ARENA_HOME/config/suramadu.config.template" > "$ARENA_RUN/suramadu/suramadu.config" # One Suramadu app per line of clients.tsv, at / - a single root-level # segment, because that is a hard requirement: Suramadu serves HTTP by # longest prefix, so a nested path renders its page fine, but its WebSocket # routing only resolves single-segment app paths. /play/ answered the # page and 404'd the socket, which the browser reports as a lost connection # (verified against the live war: /play/x/async/swing-bin 404s, # /did:plc:x/async/swing-bin upgrades). # # Each app is cloned from the rendered template app with only the path, the # display name and the login changed - then the template is deleted, so the # per-identity paths are the only client paths the container serves. Cloned # with jq rather than kept as templates, so no seat's client can drift from # another. The DID as the path is what lets headquarters' proxy match the # caller's identity to the client - a string comparison, no lookup - and # refuse it every other seat's. CFG="$ARENA_RUN/suramadu/suramadu.config" require_cmd jq # A missing list is a broken init, not an empty match; say which file rather # than dying on a redirect. [ -f "$ARENA_RUN/clients.tsv" ] || die "clients.tsv missing; 20-identity.sh did not run" while IFS="$(printf '\t')" read -r key display; do [ -n "$key" ] || continue jq --arg key "$key" --arg name "$display" ' .["/" + $key] = (.["/megamek"] | .path = "/" + $key | .name = "MegaMek " + $name | .swingConfig.launcherConfig.args |= sub(" -player_name .*$"; " -player_name " + $name)) ' "$CFG" > "$CFG.tmp" mv "$CFG.tmp" "$CFG" done < "$ARENA_RUN/clients.tsv" # A match with no client lines would be a container nobody can reach; the # template app dying with a loud line beats one that quietly serves the # wrong login. jq -e 'keys | map(select(. != "/" and . != "/megamek")) | length > 0' "$CFG" >/dev/null \ || die "no client path rendered; clients.tsv was empty" jq 'del(."/megamek")' "$CFG" > "$CFG.tmp" mv "$CFG.tmp" "$CFG" sed -e "s|@PORT_WS@|$PORT_WS|g" \ "$ARENA_HOME/config/jetty.properties.template" > "$ARENA_RUN/suramadu/jetty.properties" cp "$ARENA_HOME/config/clientsettings.xml.template" "$MM_HOME/mmconf/clientsettings.xml" # The rules half of the same pair, read by the host rather than the client: # ScenarioV1 loads mmconf/gameoptions.xml for any scenario that does not name a # GameOptionsFile of its own. Re-seeded per launch for the same reason # clientsettings.xml is - a player who opens the game options dialog and # presses OK makes the client write this file back. cp "$ARENA_HOME/config/gameoptions.xml.template" "$MM_HOME/mmconf/gameoptions.xml" # A leftover placeholder means the client would try to log in as "@PLAYER_NAME@" # and sit in the lobby forever. Easy to catch here, hard to diagnose live. if grep -q '@[A-Z_]*@' "$ARENA_RUN/suramadu/suramadu.config" "$ARENA_RUN/suramadu/jetty.properties"; then die "unsubstituted placeholder left in the rendered config" fi log "rendered suramadu config (clients: $(paste -sd' ' < <(awk -F'\t' '{ print "/" $1 "->" $2 }' "$ARENA_RUN/clients.tsv")); web :$PORT_WS, game :$PORT_MM)"