mirror of
https://github.com/nolen777/eagle0.git
synced 2026-07-28 20:55:44 +00:00
Instrument post-action processing phases (#8722)
This commit is contained in:
@@ -169,6 +169,35 @@ class EagleHistoryAppendEvent extends Event {
|
||||
var succeeded: Boolean = false
|
||||
}
|
||||
|
||||
@Name("net.eagle0.eagle.PostActionPhase")
|
||||
@Label("Eagle Post-Action Phase")
|
||||
@Category(Array("Eagle0", "Game Processing"))
|
||||
@Enabled(true)
|
||||
@StackTrace(false)
|
||||
@Threshold("0 ms")
|
||||
class EaglePostActionPhaseEvent extends Event {
|
||||
@Label("Game ID")
|
||||
var gameId: String = ""
|
||||
|
||||
@Label("Phase")
|
||||
var phase: String = ""
|
||||
|
||||
@Label("Faction ID")
|
||||
var factionId: Int = -1
|
||||
|
||||
@Label("Action Result Count")
|
||||
var actionResultCount: Int = 0
|
||||
|
||||
@Label("Human Client Count")
|
||||
var humanClientCount: Int = 0
|
||||
|
||||
@Label("Item Count")
|
||||
var itemCount: Long = 0L
|
||||
|
||||
@Label("Succeeded")
|
||||
var succeeded: Boolean = false
|
||||
}
|
||||
|
||||
object JfrEvents {
|
||||
private val usePostgresQueryEvents = new ThreadLocal[Boolean]()
|
||||
|
||||
@@ -330,4 +359,26 @@ object JfrEvents {
|
||||
}
|
||||
} else f(event)
|
||||
}
|
||||
|
||||
def postActionPhase[T](
|
||||
gameId: => String,
|
||||
phase: String,
|
||||
factionId: Int = -1
|
||||
)(f: EaglePostActionPhaseEvent => T): T = {
|
||||
val event = new EaglePostActionPhaseEvent()
|
||||
if event.isEnabled then {
|
||||
event.gameId = gameId
|
||||
event.phase = phase
|
||||
event.factionId = factionId
|
||||
event.begin()
|
||||
try {
|
||||
val value = f(event)
|
||||
event.succeeded = true
|
||||
value
|
||||
} finally {
|
||||
event.end()
|
||||
event.commit()
|
||||
}
|
||||
} else f(event)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1292,19 +1292,29 @@ class GamesManager(
|
||||
override def receive(br: BattleResolution): PostResults = {
|
||||
SimpleTimedLogger.printLogger.logLine("Handling battle resolution")
|
||||
this.synchronized {
|
||||
synchronizedHandlePostResults(
|
||||
requiredController(br.battle.eagleGameId)
|
||||
.postBattleResolution(br)
|
||||
).postResults
|
||||
JfrEvents.postActionPhase(br.battle.eagleGameId.toHexString, "gamesManager.battleResolution.total") { event =>
|
||||
val result = synchronizedHandlePostResults(
|
||||
requiredController(br.battle.eagleGameId)
|
||||
.postBattleResolution(br)
|
||||
)
|
||||
event.actionResultCount = result.postResults.results.size
|
||||
event.humanClientCount = result.gameController.humanClients.size
|
||||
result.postResults
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override def receive(bu: BattleUpdate): PostResults =
|
||||
this.synchronized {
|
||||
synchronizedHandlePostResults(
|
||||
requiredController(bu.battle.eagleGameId)
|
||||
.postBattleUpdate(bu)
|
||||
).postResults
|
||||
JfrEvents.postActionPhase(bu.battle.eagleGameId.toHexString, "gamesManager.battleUpdate.total") { event =>
|
||||
val result = synchronizedHandlePostResults(
|
||||
requiredController(bu.battle.eagleGameId)
|
||||
.postBattleUpdate(bu)
|
||||
)
|
||||
event.actionResultCount = bu.results.size
|
||||
event.humanClientCount = result.gameController.humanClients.size
|
||||
result.postResults
|
||||
}
|
||||
}
|
||||
|
||||
override def receiveBattleReset(battle: net.eagle0.eagle.internal.shardok_battle.ShardokBattle): PostResults =
|
||||
@@ -1795,26 +1805,31 @@ class GamesManager(
|
||||
): Unit = {
|
||||
requireLoadedGame(gameId)
|
||||
this.synchronized {
|
||||
val afterHumanCommand = synchronizedHandlePostResults(
|
||||
requiredController(gameId)
|
||||
.postHumanCommandOnly(
|
||||
userId = userId,
|
||||
selectedProvinceId = selectedProvinceId,
|
||||
command = command,
|
||||
token = token
|
||||
)
|
||||
)
|
||||
synchronizedHandlePostResults(
|
||||
GameController.performAutomaticContinuation(
|
||||
GameControllerWithPostResults(
|
||||
gameController = afterHumanCommand.gameController,
|
||||
postResults = PostResults(gameState = None, battles = Vector.empty, results = Vector.empty)
|
||||
JfrEvents.postActionPhase(gameId.toHexString, "gamesManager.humanCommand.total") { event =>
|
||||
val afterHumanCommand = synchronizedHandlePostResults(
|
||||
requiredController(gameId)
|
||||
.postHumanCommandOnly(
|
||||
userId = userId,
|
||||
selectedProvinceId = selectedProvinceId,
|
||||
command = command,
|
||||
token = token
|
||||
)
|
||||
)
|
||||
val afterContinuation = synchronizedHandlePostResults(
|
||||
GameController.performAutomaticContinuation(
|
||||
GameControllerWithPostResults(
|
||||
gameController = afterHumanCommand.gameController,
|
||||
postResults = PostResults(gameState = None, battles = Vector.empty, results = Vector.empty)
|
||||
)
|
||||
)
|
||||
)
|
||||
): Unit
|
||||
// Record last played time for this user
|
||||
updateLastPlayed(gameId, userId)
|
||||
save()
|
||||
// Record last played time for this user
|
||||
updateLastPlayed(gameId, userId)
|
||||
save()
|
||||
event.actionResultCount =
|
||||
afterHumanCommand.postResults.results.size + afterContinuation.postResults.results.size
|
||||
event.humanClientCount = afterContinuation.gameController.humanClients.size
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1827,18 +1842,22 @@ class GamesManager(
|
||||
): Unit = {
|
||||
requireLoadedGame(gameId)
|
||||
this.synchronized {
|
||||
synchronizedHandlePostResults(
|
||||
requiredController(gameId)
|
||||
.postHumanCommandOnly(
|
||||
userId = userId,
|
||||
selectedProvinceId = selectedProvinceId,
|
||||
command = command,
|
||||
token = token
|
||||
)
|
||||
): Unit
|
||||
// Record last played time for this user
|
||||
updateLastPlayed(gameId, userId)
|
||||
save()
|
||||
JfrEvents.postActionPhase(gameId.toHexString, "gamesManager.humanCommandWithoutContinuation.total") { event =>
|
||||
val result = synchronizedHandlePostResults(
|
||||
requiredController(gameId)
|
||||
.postHumanCommandOnly(
|
||||
userId = userId,
|
||||
selectedProvinceId = selectedProvinceId,
|
||||
command = command,
|
||||
token = token
|
||||
)
|
||||
)
|
||||
// Record last played time for this user
|
||||
updateLastPlayed(gameId, userId)
|
||||
save()
|
||||
event.actionResultCount = result.postResults.results.size
|
||||
event.humanClientCount = result.gameController.humanClients.size
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2072,6 +2091,18 @@ class GamesManager(
|
||||
private def synchronizedHandlePostResults(
|
||||
cwrt: GameControllerWithPostResults,
|
||||
handleUnrequestedTexts: Boolean
|
||||
): GameControllerWithPostResults =
|
||||
JfrEvents.postActionPhase(cwrt.gameController.gameId.toHexString, "gamesManager.handlePostResults") { event =>
|
||||
val result = synchronizedHandlePostResultsImpl(cwrt, handleUnrequestedTexts)
|
||||
event.actionResultCount = result.postResults.results.size
|
||||
event.humanClientCount = result.gameController.humanClients.size
|
||||
event.itemCount = result.postResults.battles.size
|
||||
result
|
||||
}
|
||||
|
||||
private def synchronizedHandlePostResultsImpl(
|
||||
cwrt: GameControllerWithPostResults,
|
||||
handleUnrequestedTexts: Boolean
|
||||
): GameControllerWithPostResults = {
|
||||
val newController = cwrt.postResults match {
|
||||
case PostResults(
|
||||
|
||||
@@ -295,15 +295,31 @@ object GameController {
|
||||
def performAutomaticContinuation(
|
||||
gameControllerWithPostResults: GameControllerWithPostResults
|
||||
): GameControllerWithPostResults = {
|
||||
val automaticUpdates = gameControllerWithPostResults.gameController
|
||||
.withHandledEngineAndResults(gameControllerWithPostResults.gameController.engine.updated)
|
||||
val controller = gameControllerWithPostResults.gameController
|
||||
lazy val gameIdString = controller.gameId.toHexString
|
||||
val engineUpdate = JfrEvents.postActionPhase(gameIdString, "automaticContinuation.engineUpdate") { event =>
|
||||
val updated = controller.engine.updated
|
||||
event.actionResultCount = updated.results.size
|
||||
updated
|
||||
}
|
||||
val automaticUpdates = JfrEvents.postActionPhase(gameIdString, "automaticContinuation.handleResults") { event =>
|
||||
val handled = controller.withHandledEngineAndResults(engineUpdate)
|
||||
event.actionResultCount = handled.postResults.results.size
|
||||
event.humanClientCount = handled.gameController.humanClients.size
|
||||
handled
|
||||
}
|
||||
|
||||
performAiCommands(
|
||||
GameControllerWithPostResults(
|
||||
gameController = automaticUpdates.gameController,
|
||||
postResults = gameControllerWithPostResults.postResults.append(automaticUpdates.postResults)
|
||||
JfrEvents.postActionPhase(gameIdString, "automaticContinuation.performAiCommands") { event =>
|
||||
val result = performAiCommands(
|
||||
GameControllerWithPostResults(
|
||||
gameController = automaticUpdates.gameController,
|
||||
postResults = gameControllerWithPostResults.postResults.append(automaticUpdates.postResults)
|
||||
)
|
||||
)
|
||||
)
|
||||
event.actionResultCount = result.postResults.results.size
|
||||
event.humanClientCount = result.gameController.humanClients.size
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
private def humanClientsAfterUpdatingLlmStream(
|
||||
@@ -358,28 +374,32 @@ object GameController {
|
||||
// Send battle progress updates if there is any progress to report
|
||||
if battleProgress.isEmpty then afterResults
|
||||
else {
|
||||
val factions = engine.currentState.factions.values.toVector
|
||||
val factions = engine.currentState.factions.values.toVector
|
||||
lazy val gameIdString = engine.gameId.toHexString
|
||||
afterResults.map { client =>
|
||||
val firstFightableBattleId =
|
||||
GameStateViewFilter.firstFightableBattle(engine.currentState, client.factionId).map(_.shardokGameId)
|
||||
val activeObservableBattleIds =
|
||||
GameStateViewFilter.activeObservableBattleIds(engine.currentState, client.factionId).toSet
|
||||
val enrichedBattles = GameStateViewFilter
|
||||
.orderedBattles(engine.currentState)
|
||||
.filter(battle => battleProgress.contains(battle.shardokGameId))
|
||||
.map { battle =>
|
||||
val isAllied = BattleFilter.isParticipantOrAlly(client.factionId, battle, factions)
|
||||
val view =
|
||||
BattleFilter.filterBattle(battle, factions, Some(client.factionId), engine.currentState.battalions)
|
||||
GameStateViewFilter.markBattleAccess(
|
||||
enrichBattleView(view, battleProgress, lastRevealedRound, isAllied),
|
||||
firstFightableBattleId,
|
||||
activeObservableBattleIds
|
||||
)
|
||||
JfrEvents.postActionPhase(gameIdString, "client.battleProgress", client.factionId) { event =>
|
||||
val firstFightableBattleId =
|
||||
GameStateViewFilter.firstFightableBattle(engine.currentState, client.factionId).map(_.shardokGameId)
|
||||
val activeObservableBattleIds =
|
||||
GameStateViewFilter.activeObservableBattleIds(engine.currentState, client.factionId).toSet
|
||||
val enrichedBattles = GameStateViewFilter
|
||||
.orderedBattles(engine.currentState)
|
||||
.filter(battle => battleProgress.contains(battle.shardokGameId))
|
||||
.map { battle =>
|
||||
val isAllied = BattleFilter.isParticipantOrAlly(client.factionId, battle, factions)
|
||||
val view =
|
||||
BattleFilter.filterBattle(battle, factions, Some(client.factionId), engine.currentState.battalions)
|
||||
GameStateViewFilter.markBattleAccess(
|
||||
enrichBattleView(view, battleProgress, lastRevealedRound, isAllied),
|
||||
firstFightableBattleId,
|
||||
activeObservableBattleIds
|
||||
)
|
||||
}
|
||||
event.itemCount = enrichedBattles.size
|
||||
try client.sendBattleProgress(enrichedBattles, lastRevealedRound)
|
||||
catch {
|
||||
case _: StatusRuntimeException | _: IllegalStateException => client
|
||||
}
|
||||
try client.sendBattleProgress(enrichedBattles, lastRevealedRound)
|
||||
catch {
|
||||
case _: StatusRuntimeException | _: IllegalStateException => client
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -393,87 +413,107 @@ object GameController {
|
||||
engine: Engine,
|
||||
fullHistory: FullGameHistory,
|
||||
aiFactionIds: Set[FactionId]
|
||||
): Vector[HumanPlayerClientConnectionState] =
|
||||
): Vector[HumanPlayerClientConnectionState] = {
|
||||
lazy val gameIdString = engine.gameId.toHexString
|
||||
humanClients.flatMap { client =>
|
||||
val clientTextUpdates = visibilityExtensions
|
||||
.filter(_.recipientFactionIds.contains(client.factionId))
|
||||
.flatMap { ext =>
|
||||
clientTextStore.getText(ext.textId) match {
|
||||
case TextGenerationSuccess(text) =>
|
||||
Some(CompleteClientText(id = ext.textId, text = text, requestedAfterHistoryCount = 0))
|
||||
case _ => None
|
||||
val clientTextUpdates = JfrEvents.postActionPhase(gameIdString, "client.textUpdates", client.factionId) { event =>
|
||||
val updates = visibilityExtensions
|
||||
.filter(_.recipientFactionIds.contains(client.factionId))
|
||||
.flatMap { ext =>
|
||||
clientTextStore.getText(ext.textId) match {
|
||||
case TextGenerationSuccess(text) =>
|
||||
Some(CompleteClientText(id = ext.textId, text = text, requestedAfterHistoryCount = 0))
|
||||
case _ => None
|
||||
}
|
||||
}
|
||||
}
|
||||
event.itemCount = updates.size
|
||||
updates
|
||||
}
|
||||
|
||||
val updates = engine.filteredHistoryWithUnfilteredCount(
|
||||
client.factionId,
|
||||
client.unfilteredKnownHistoryCount
|
||||
)
|
||||
val outstandingBattleIds =
|
||||
GameStateViewFilter.activeObservableBattleIds(engine.currentState, client.factionId)
|
||||
val fightableBattleIds =
|
||||
GameStateViewFilter.activeFightableBattleIds(engine.currentState, client.factionId).toSet
|
||||
val shardokGameIdsToCheck =
|
||||
outstandingBattleIds.distinct
|
||||
val updates = JfrEvents.postActionPhase(gameIdString, "client.historyProjection", client.factionId) { event =>
|
||||
val historyUpdates = engine.filteredHistoryWithUnfilteredCount(
|
||||
client.factionId,
|
||||
client.unfilteredKnownHistoryCount
|
||||
)
|
||||
event.itemCount = historyUpdates.filteredResults.size
|
||||
historyUpdates
|
||||
}
|
||||
|
||||
val shardokUpdates =
|
||||
shardokGameIdsToCheck.map { shardokGameId =>
|
||||
val startingCount = client.knownShardokResultCounts
|
||||
.find(_.shardokGameId == shardokGameId)
|
||||
.map(_.filteredResultCount)
|
||||
.getOrElse(0)
|
||||
val arvs = fullHistory.shardokPlayerResultsSince(
|
||||
shardokGameId = shardokGameId,
|
||||
factionId = client.factionId,
|
||||
startingCount = startingCount
|
||||
)
|
||||
val acs = fullHistory
|
||||
.shardokPlayerAvailableCommands(
|
||||
val shardokUpdates = JfrEvents.postActionPhase(gameIdString, "client.shardokProjection", client.factionId) {
|
||||
event =>
|
||||
val outstandingBattleIds =
|
||||
GameStateViewFilter.activeObservableBattleIds(engine.currentState, client.factionId)
|
||||
val fightableBattleIds =
|
||||
GameStateViewFilter.activeFightableBattleIds(engine.currentState, client.factionId).toSet
|
||||
val shardokGameIdsToCheck = outstandingBattleIds.distinct
|
||||
|
||||
val updates = shardokGameIdsToCheck.map { shardokGameId =>
|
||||
val startingCount = client.knownShardokResultCounts
|
||||
.find(_.shardokGameId == shardokGameId)
|
||||
.map(_.filteredResultCount)
|
||||
.getOrElse(0)
|
||||
val arvs = fullHistory.shardokPlayerResultsSince(
|
||||
shardokGameId = shardokGameId,
|
||||
factionId = client.factionId
|
||||
factionId = client.factionId,
|
||||
startingCount = startingCount
|
||||
)
|
||||
.filter(_ => fightableBattleIds.contains(shardokGameId))
|
||||
val shardokStatus = GameController.computeShardokServerStatus(
|
||||
shardokGameId = shardokGameId,
|
||||
myFactionId = client.factionId,
|
||||
fullHistory = fullHistory,
|
||||
allFactionIds = engine.factionIds,
|
||||
aiFactionIds = aiFactionIds
|
||||
)
|
||||
val allArvs = fullHistory.shardokPlayerResultsSince(
|
||||
shardokGameId = shardokGameId,
|
||||
factionId = client.factionId,
|
||||
startingCount = 0
|
||||
)
|
||||
val tutorialDialogues = client.selectTacticalTutorialDialogues(
|
||||
candidates = TacticalTutorialDialogueSelector.candidates(
|
||||
gameState = engine.currentState,
|
||||
allResultViews = allArvs,
|
||||
newResultViewCount = arvs.size,
|
||||
availableCommands = acs
|
||||
val acs = fullHistory
|
||||
.shardokPlayerAvailableCommands(
|
||||
shardokGameId = shardokGameId,
|
||||
factionId = client.factionId
|
||||
)
|
||||
.filter(_ => fightableBattleIds.contains(shardokGameId))
|
||||
val shardokStatus = GameController.computeShardokServerStatus(
|
||||
shardokGameId = shardokGameId,
|
||||
myFactionId = client.factionId,
|
||||
fullHistory = fullHistory,
|
||||
allFactionIds = engine.factionIds,
|
||||
aiFactionIds = aiFactionIds
|
||||
)
|
||||
)
|
||||
SingleShardokGameResultResponse(
|
||||
shardokGameId = shardokGameId,
|
||||
actionResultViews = arvs,
|
||||
newResultViewCount = startingCount + arvs.length,
|
||||
availableCommands = acs,
|
||||
shardokServerStatus = Some(shardokStatus),
|
||||
tutorialDialogues = tutorialDialogues.map(ActionResultViewConverter.tutorialDialogueToProto)
|
||||
)
|
||||
}
|
||||
val allArvs = fullHistory.shardokPlayerResultsSince(
|
||||
shardokGameId = shardokGameId,
|
||||
factionId = client.factionId,
|
||||
startingCount = 0
|
||||
)
|
||||
val tutorialDialogues = client.selectTacticalTutorialDialogues(
|
||||
candidates = TacticalTutorialDialogueSelector.candidates(
|
||||
gameState = engine.currentState,
|
||||
allResultViews = allArvs,
|
||||
newResultViewCount = arvs.size,
|
||||
availableCommands = acs
|
||||
)
|
||||
)
|
||||
SingleShardokGameResultResponse(
|
||||
shardokGameId = shardokGameId,
|
||||
actionResultViews = arvs,
|
||||
newResultViewCount = startingCount + arvs.length,
|
||||
availableCommands = acs,
|
||||
shardokServerStatus = Some(shardokStatus),
|
||||
tutorialDialogues = tutorialDialogues.map(ActionResultViewConverter.tutorialDialogueToProto)
|
||||
)
|
||||
}
|
||||
event.itemCount = updates.size
|
||||
updates
|
||||
}
|
||||
|
||||
try
|
||||
Some {
|
||||
val scalaCommands = engine.getAvailablePlayerCommands(client.factionId)
|
||||
val availableCommands = GameController.toProtoAvailableCommands(scalaCommands, engine, client.factionId)
|
||||
val availableCommands =
|
||||
JfrEvents.postActionPhase(gameIdString, "client.availableCommands", client.factionId) { event =>
|
||||
val scalaCommands = engine.getAvailablePlayerCommands(client.factionId)
|
||||
event.itemCount = scalaCommands.size
|
||||
GameController.toProtoAvailableCommands(scalaCommands, engine, client.factionId)
|
||||
}
|
||||
|
||||
val serverGameStatus = GameController.computeServerGameStatus(
|
||||
engine = engine,
|
||||
factionId = client.factionId,
|
||||
otherFactionIds = engine.factionIds.filterNot(_ == client.factionId),
|
||||
clientTextStore = clientTextStore
|
||||
)
|
||||
val serverGameStatus =
|
||||
JfrEvents.postActionPhase(gameIdString, "client.serverStatus", client.factionId) { _ =>
|
||||
GameController.computeServerGameStatus(
|
||||
engine = engine,
|
||||
factionId = client.factionId,
|
||||
otherFactionIds = engine.factionIds.filterNot(_ == client.factionId),
|
||||
clientTextStore = clientTextStore
|
||||
)
|
||||
}
|
||||
|
||||
val note =
|
||||
s"fid ${client.factionId}: Updating client with ${updates.filteredResults.length} new results, " +
|
||||
@@ -485,18 +525,21 @@ object GameController {
|
||||
SimpleTimedLogger
|
||||
.fileLogger(gameId = engine.gameId, fileName = "timings")
|
||||
.withStopwatch(note + note2) {
|
||||
clientTextUpdates.foldLeft(
|
||||
client
|
||||
.update(
|
||||
availableCommands = availableCommands,
|
||||
filteredResults = updates.filteredResults,
|
||||
unfilteredCountAfter = updates.unfilteredCountAfter,
|
||||
knownShardokResults = shardokUpdates.toVector,
|
||||
serverGameStatus = serverGameStatus
|
||||
)
|
||||
) {
|
||||
case (client, clientTextUpdate) =>
|
||||
client.update(clientText = clientTextUpdate)
|
||||
JfrEvents.postActionPhase(gameIdString, "client.dispatch", client.factionId) { event =>
|
||||
event.itemCount = updates.filteredResults.size + shardokUpdates.size + clientTextUpdates.size
|
||||
clientTextUpdates.foldLeft(
|
||||
client
|
||||
.update(
|
||||
availableCommands = availableCommands,
|
||||
filteredResults = updates.filteredResults,
|
||||
unfilteredCountAfter = updates.unfilteredCountAfter,
|
||||
knownShardokResults = shardokUpdates.toVector,
|
||||
serverGameStatus = serverGameStatus
|
||||
)
|
||||
) {
|
||||
case (client, clientTextUpdate) =>
|
||||
client.update(clientText = clientTextUpdate)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -516,6 +559,7 @@ object GameController {
|
||||
}
|
||||
end try
|
||||
}
|
||||
}
|
||||
|
||||
/** Update battle progress tracking from Shardok game update data. */
|
||||
private[controller] def updatedBattleProgress(
|
||||
@@ -666,8 +710,13 @@ final case class GameController(
|
||||
|
||||
// FIXME: this isn't actually atomic, but at least we'll fail violently
|
||||
private def copyWithAtomicSave: GameController = try {
|
||||
val newCts = clientTextStore.saved
|
||||
val newEngine = engine.saveNow
|
||||
lazy val gameIdString = gameId.toHexString
|
||||
val newCts = JfrEvents.postActionPhase(gameIdString, "save.clientText") { _ =>
|
||||
clientTextStore.saved
|
||||
}
|
||||
val newEngine = JfrEvents.postActionPhase(gameIdString, "save.history") { _ =>
|
||||
engine.saveNow
|
||||
}
|
||||
|
||||
copy(
|
||||
engine = newEngine,
|
||||
@@ -688,24 +737,34 @@ final case class GameController(
|
||||
save: Boolean,
|
||||
runUpdateChecks: Boolean = true
|
||||
): GameControllerWithPostResults = {
|
||||
val engineAndResults =
|
||||
if runUpdateChecks then
|
||||
this.engine.postCommand(
|
||||
factionId = factionId,
|
||||
selectedProvinceId = selectedProvinceId,
|
||||
selectedCommand = command
|
||||
)
|
||||
else
|
||||
this.engine.postCommandWithoutUpdateChecks(
|
||||
factionId = factionId,
|
||||
selectedProvinceId = selectedProvinceId,
|
||||
selectedCommand = command
|
||||
)
|
||||
lazy val gameIdString = gameId.toHexString
|
||||
val engineAndResults = JfrEvents.postActionPhase(gameIdString, "command.engine", factionId) { event =>
|
||||
val result =
|
||||
if runUpdateChecks then
|
||||
this.engine.postCommand(
|
||||
factionId = factionId,
|
||||
selectedProvinceId = selectedProvinceId,
|
||||
selectedCommand = command
|
||||
)
|
||||
else
|
||||
this.engine.postCommandWithoutUpdateChecks(
|
||||
factionId = factionId,
|
||||
selectedProvinceId = selectedProvinceId,
|
||||
selectedCommand = command
|
||||
)
|
||||
event.actionResultCount = result.results.size
|
||||
result
|
||||
}
|
||||
|
||||
withHandledEngineAndResults(
|
||||
engineAndResults = engineAndResults,
|
||||
updateClientsAndSave = save
|
||||
)
|
||||
JfrEvents.postActionPhase(gameIdString, "command.handleResults", factionId) { event =>
|
||||
val result = withHandledEngineAndResults(
|
||||
engineAndResults = engineAndResults,
|
||||
updateClientsAndSave = save
|
||||
)
|
||||
event.actionResultCount = result.postResults.results.size
|
||||
event.humanClientCount = result.gameController.humanClients.size
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
def withNewAiClientWithFid(fid: FactionId): GameController =
|
||||
@@ -720,10 +779,13 @@ final case class GameController(
|
||||
def postBattleResolution(
|
||||
battleResolution: BattleResolution
|
||||
): GameControllerWithPostResults = {
|
||||
val shardokGameId = battleResolution.battle.shardokGameId
|
||||
val shardokGameId = battleResolution.battle.shardokGameId
|
||||
lazy val gameIdString = gameId.toHexString
|
||||
|
||||
// A completed strategic battle must never become durable before its final tactical updates.
|
||||
fullHistory.flushShardokWrites()
|
||||
JfrEvents.postActionPhase(gameIdString, "battleResolution.flushShardokWrites") { _ =>
|
||||
fullHistory.flushShardokWrites()
|
||||
}
|
||||
|
||||
// Record the winner in the battle progress tracker
|
||||
val winnerFid = battleResolution.resolvedPlayers
|
||||
@@ -742,32 +804,48 @@ final case class GameController(
|
||||
case None => battleProgress
|
||||
}
|
||||
|
||||
val result = GameController.performAiCommands(
|
||||
withHandledEngineAndResults(
|
||||
engineAndResults = engine.resolveBattle(battleResolution)
|
||||
val engineAndResults = JfrEvents.postActionPhase(gameIdString, "battleResolution.resolveEngine") { event =>
|
||||
val resolved = engine.resolveBattle(battleResolution)
|
||||
event.actionResultCount = resolved.results.size
|
||||
resolved
|
||||
}
|
||||
val handledResults = JfrEvents.postActionPhase(gameIdString, "battleResolution.handleResults") { event =>
|
||||
val handled = withHandledEngineAndResults(engineAndResults = engineAndResults)
|
||||
event.actionResultCount = handled.postResults.results.size
|
||||
event.humanClientCount = handled.gameController.humanClients.size
|
||||
handled
|
||||
}
|
||||
val result = JfrEvents.postActionPhase(gameIdString, "battleResolution.performAiCommands") { event =>
|
||||
val withAiCommands = GameController.performAiCommands(handledResults)
|
||||
event.actionResultCount = withAiCommands.postResults.results.size
|
||||
event.humanClientCount = withAiCommands.gameController.humanClients.size
|
||||
withAiCommands
|
||||
}
|
||||
|
||||
JfrEvents.postActionPhase(gameIdString, "battleResolution.cleanup") { event =>
|
||||
// Clean up: remove progress for battles that are no longer outstanding
|
||||
val remainingBattleIds = result.gameController.engine.currentState.outstandingBattles
|
||||
.map(_.shardokGameId)
|
||||
.toSet
|
||||
val cleanedProgress = updatedProgress.filter { case (id, _) => remainingBattleIds.contains(id) }
|
||||
|
||||
if !remainingBattleIds.contains(shardokGameId) then
|
||||
result.gameController.fullHistory.evictShardokReadCache(shardokGameId)
|
||||
|
||||
// Reset gated round when all battles have ended so the next battle starts at day 0
|
||||
val cleanedLastRevealedRound =
|
||||
if cleanedProgress.isEmpty then 0
|
||||
else result.gameController.lastRevealedRound
|
||||
|
||||
event.actionResultCount = result.postResults.results.size
|
||||
event.itemCount = remainingBattleIds.size
|
||||
result.copy(
|
||||
gameController = result.gameController.copy(
|
||||
battleProgress = cleanedProgress,
|
||||
lastRevealedRound = cleanedLastRevealedRound
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
// Clean up: remove progress for battles that are no longer outstanding
|
||||
val remainingBattleIds = result.gameController.engine.currentState.outstandingBattles
|
||||
.map(_.shardokGameId)
|
||||
.toSet
|
||||
val cleanedProgress = updatedProgress.filter { case (id, _) => remainingBattleIds.contains(id) }
|
||||
|
||||
if !remainingBattleIds.contains(shardokGameId) then
|
||||
result.gameController.fullHistory.evictShardokReadCache(shardokGameId)
|
||||
|
||||
// Reset gated round when all battles have ended so the next battle starts at day 0
|
||||
val cleanedLastRevealedRound =
|
||||
if cleanedProgress.isEmpty then 0
|
||||
else result.gameController.lastRevealedRound
|
||||
|
||||
result.copy(
|
||||
gameController = result.gameController.copy(
|
||||
battleProgress = cleanedProgress,
|
||||
lastRevealedRound = cleanedLastRevealedRound
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
def applyTutorialSetup(results: Vector[ActionResultT]): GameControllerWithPostResults =
|
||||
@@ -853,18 +931,22 @@ final case class GameController(
|
||||
s"We should have ${battleUpdate.newUnfilteredCount} results, but we had $oldCount and got ${battleUpdate.results.size} new ones"
|
||||
)
|
||||
|
||||
val newHistory = fullHistory.withNewShardokResults(
|
||||
shardokGameId = battleUpdate.battle.shardokGameId,
|
||||
newResults = battleUpdate.results,
|
||||
newPlayerResults = battleUpdate.updates.map { opr =>
|
||||
(opr.eagleFactionId, opr.actionResultViews.toVector)
|
||||
},
|
||||
eagleRoundId = battleUpdate.battle.roundStarted,
|
||||
currentGameState = battleUpdate.currentGameStateBytes,
|
||||
availableCommands = battleUpdate.updates.map { opr =>
|
||||
(opr.eagleFactionId, opr.availableCommands)
|
||||
}
|
||||
)
|
||||
val newHistory = JfrEvents.postActionPhase(gameId.toHexString, "battleUpdate.acceptResults") { event =>
|
||||
event.actionResultCount = battleUpdate.results.size
|
||||
event.itemCount = battleUpdate.updates.size
|
||||
fullHistory.withNewShardokResults(
|
||||
shardokGameId = battleUpdate.battle.shardokGameId,
|
||||
newResults = battleUpdate.results,
|
||||
newPlayerResults = battleUpdate.updates.map { opr =>
|
||||
(opr.eagleFactionId, opr.actionResultViews.toVector)
|
||||
},
|
||||
eagleRoundId = battleUpdate.battle.roundStarted,
|
||||
currentGameState = battleUpdate.currentGameStateBytes,
|
||||
availableCommands = battleUpdate.updates.map { opr =>
|
||||
(opr.eagleFactionId, opr.availableCommands)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
val newEngine = engine.withHistory(newHistory)
|
||||
|
||||
@@ -881,7 +963,9 @@ final case class GameController(
|
||||
|
||||
// Only send battle progress when the gated round advances, to avoid flooding
|
||||
// clients with per-action updates where the day hasn't changed
|
||||
val updatedHumanClients =
|
||||
val updatedHumanClients = JfrEvents.postActionPhase(gameId.toHexString, "battleUpdate.updateClients") { event =>
|
||||
event.actionResultCount = battleUpdate.results.size
|
||||
event.humanClientCount = humanClients.size
|
||||
if newLastRevealedRound > lastRevealedRound then
|
||||
GameController.humanClientsAfterPostingResults(
|
||||
humanClients = humanClients,
|
||||
@@ -902,6 +986,8 @@ final case class GameController(
|
||||
fullHistory = newHistory,
|
||||
aiFactionIds = aiClientFactionIds.toSet
|
||||
)
|
||||
end if
|
||||
}
|
||||
|
||||
GameControllerWithPostResults(
|
||||
gameController = copy(
|
||||
@@ -1128,52 +1214,66 @@ final case class GameController(
|
||||
engineAndResults: EngineAndResults,
|
||||
updateClientsAndSave: Boolean = true
|
||||
): GameControllerWithPostResults = {
|
||||
lazy val gameIdString = gameId.toHexString
|
||||
// Check for tutorial strategic events that should fire based on the new game state
|
||||
val tutorialEvents = TutorialStrategicEvents.checkForEvents(engineAndResults.engine.currentState)
|
||||
val finalEngineAndResults =
|
||||
if tutorialEvents.isEmpty then engineAndResults
|
||||
else engineAndResults.engine.applyActionResults(tutorialEvents)
|
||||
val finalEngineAndResults = JfrEvents.postActionPhase(gameIdString, "results.tutorialEvents") { event =>
|
||||
val tutorialEvents = TutorialStrategicEvents.checkForEvents(engineAndResults.engine.currentState)
|
||||
event.itemCount = tutorialEvents.size
|
||||
val result =
|
||||
if tutorialEvents.isEmpty then engineAndResults
|
||||
else engineAndResults.engine.applyActionResults(tutorialEvents)
|
||||
event.actionResultCount = result.results.size
|
||||
result
|
||||
}
|
||||
|
||||
val llmRequestsWithHistoryCounts = allNewLlmRequests(
|
||||
finalEngineAndResults.engine.history.count - finalEngineAndResults.results.length,
|
||||
finalEngineAndResults.results
|
||||
)
|
||||
val (llmRequestsWithHistoryCounts, clientVisibilityExtensions) =
|
||||
JfrEvents.postActionPhase(gameIdString, "results.collectClientText") { event =>
|
||||
val requests = allNewLlmRequests(
|
||||
finalEngineAndResults.engine.history.count - finalEngineAndResults.results.length,
|
||||
finalEngineAndResults.results
|
||||
)
|
||||
val extensions = finalEngineAndResults.results.flatMap(_.clientTextVisibilityExtensions)
|
||||
event.actionResultCount = finalEngineAndResults.results.size
|
||||
event.itemCount = requests.size + extensions.size
|
||||
requests -> extensions
|
||||
}
|
||||
|
||||
val clientVisibilityExtensions =
|
||||
finalEngineAndResults.results.flatMap(_.clientTextVisibilityExtensions)
|
||||
|
||||
val newFullHistory = extractFullHistory(finalEngineAndResults.engine)
|
||||
|
||||
val handledGc = copy(
|
||||
engine = finalEngineAndResults.engine,
|
||||
fullHistory = newFullHistory,
|
||||
userIdToFactionId = userIdToFactionId.filter {
|
||||
case (_, fid) =>
|
||||
finalEngineAndResults.engine.factionIds.contains(fid)
|
||||
},
|
||||
humanClients = humanClients,
|
||||
aiClients = {
|
||||
val afterRemovals = aiClients
|
||||
.filterNot(aic =>
|
||||
finalEngineAndResults.results
|
||||
.exists(_.removedFactionIds.contains(aic.newValue.factionId))
|
||||
)
|
||||
val existingFids = afterRemovals.map(_.newValue.factionId).toSet ++ userIdToFactionId.values.toSet
|
||||
val newAiFids = finalEngineAndResults.results
|
||||
.flatMap(_.newFactions.map(_.id))
|
||||
.filterNot(existingFids.contains)
|
||||
newAiFids.foldLeft(afterRemovals) { (clients, fid) =>
|
||||
clients :+ RandomState(
|
||||
AIClient(gameId = gameId, factionId = fid),
|
||||
SeededRandom(GameController.aiSeed(engine.gameId, fid))
|
||||
)
|
||||
}
|
||||
},
|
||||
clientTextStore = foldInExtendedVisibility(
|
||||
foldInLlmRequests(this.clientTextStore, llmRequestsWithHistoryCounts),
|
||||
clientVisibilityExtensions
|
||||
val handledGc = JfrEvents.postActionPhase(gameIdString, "results.prepareController") { event =>
|
||||
val newFullHistory = extractFullHistory(finalEngineAndResults.engine)
|
||||
val controller = copy(
|
||||
engine = finalEngineAndResults.engine,
|
||||
fullHistory = newFullHistory,
|
||||
userIdToFactionId = userIdToFactionId.filter {
|
||||
case (_, fid) =>
|
||||
finalEngineAndResults.engine.factionIds.contains(fid)
|
||||
},
|
||||
humanClients = humanClients,
|
||||
aiClients = {
|
||||
val afterRemovals = aiClients
|
||||
.filterNot(aic =>
|
||||
finalEngineAndResults.results
|
||||
.exists(_.removedFactionIds.contains(aic.newValue.factionId))
|
||||
)
|
||||
val existingFids = afterRemovals.map(_.newValue.factionId).toSet ++ userIdToFactionId.values.toSet
|
||||
val newAiFids = finalEngineAndResults.results
|
||||
.flatMap(_.newFactions.map(_.id))
|
||||
.filterNot(existingFids.contains)
|
||||
newAiFids.foldLeft(afterRemovals) { (clients, fid) =>
|
||||
clients :+ RandomState(
|
||||
AIClient(gameId = gameId, factionId = fid),
|
||||
SeededRandom(GameController.aiSeed(engine.gameId, fid))
|
||||
)
|
||||
}
|
||||
},
|
||||
clientTextStore = foldInExtendedVisibility(
|
||||
foldInLlmRequests(this.clientTextStore, llmRequestsWithHistoryCounts),
|
||||
clientVisibilityExtensions
|
||||
)
|
||||
)
|
||||
)
|
||||
event.actionResultCount = finalEngineAndResults.results.size
|
||||
event.humanClientCount = controller.humanClients.size
|
||||
controller
|
||||
}
|
||||
|
||||
val newGc =
|
||||
if updateClientsAndSave then {
|
||||
@@ -1199,18 +1299,22 @@ final case class GameController(
|
||||
results: Seq[ActionResultT],
|
||||
clientTextStoreForUpdate: ClientTextStore
|
||||
): GameController =
|
||||
copy(
|
||||
humanClients = GameController.humanClientsAfterPostingResults(
|
||||
humanClients = humanClients,
|
||||
clientTextStore = clientTextStoreForUpdate,
|
||||
visibilityExtensions = results.flatMap(_.clientTextVisibilityExtensions).toVector,
|
||||
engine = engine,
|
||||
fullHistory = fullHistory,
|
||||
aiFactionIds = aiClientFactionIds.toSet,
|
||||
battleProgress = battleProgress,
|
||||
lastRevealedRound = lastRevealedRound
|
||||
JfrEvents.postActionPhase(gameId.toHexString, "clients.updateAll") { event =>
|
||||
event.actionResultCount = results.size
|
||||
event.humanClientCount = humanClients.size
|
||||
copy(
|
||||
humanClients = GameController.humanClientsAfterPostingResults(
|
||||
humanClients = humanClients,
|
||||
clientTextStore = clientTextStoreForUpdate,
|
||||
visibilityExtensions = results.flatMap(_.clientTextVisibilityExtensions).toVector,
|
||||
engine = engine,
|
||||
fullHistory = fullHistory,
|
||||
aiFactionIds = aiClientFactionIds.toSet,
|
||||
battleProgress = battleProgress,
|
||||
lastRevealedRound = lastRevealedRound
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
def streamUpdates(
|
||||
userId: UserId,
|
||||
@@ -1441,7 +1545,12 @@ final case class GameController(
|
||||
}
|
||||
val historyCountBeforeCommand = engine.history.count
|
||||
val gameStateBeforeCommand = engine.currentState
|
||||
val availableCommands = engine.getAvailablePlayerCommands(client.factionId)
|
||||
val availableCommands =
|
||||
JfrEvents.postActionPhase(gameId.toHexString, "humanCommand.availableCommandsForLog", client.factionId) { event =>
|
||||
val commands = engine.getAvailablePlayerCommands(client.factionId)
|
||||
event.itemCount = commands.size
|
||||
commands
|
||||
}
|
||||
val result = this
|
||||
.withPostedCommand(
|
||||
factionId = client.factionId,
|
||||
@@ -1451,15 +1560,18 @@ final case class GameController(
|
||||
runUpdateChecks = false
|
||||
)
|
||||
|
||||
AIDecisionRecordLogger.logHumanCommand(
|
||||
gameStateBeforeCommand = gameStateBeforeCommand,
|
||||
firstActionSeq = historyCountBeforeCommand,
|
||||
factionId = client.factionId,
|
||||
provinceId = selectedProvinceId,
|
||||
selectedCommand = selectedCommand,
|
||||
actionResults = result.postResults.results,
|
||||
availableCommands = availableCommands
|
||||
)
|
||||
JfrEvents.postActionPhase(gameId.toHexString, "humanCommand.decisionLogging", client.factionId) { event =>
|
||||
event.actionResultCount = result.postResults.results.size
|
||||
AIDecisionRecordLogger.logHumanCommand(
|
||||
gameStateBeforeCommand = gameStateBeforeCommand,
|
||||
firstActionSeq = historyCountBeforeCommand,
|
||||
factionId = client.factionId,
|
||||
provinceId = selectedProvinceId,
|
||||
selectedCommand = selectedCommand,
|
||||
actionResults = result.postResults.results,
|
||||
availableCommands = availableCommands
|
||||
)
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
@@ -18,6 +18,14 @@ scala_library(
|
||||
],
|
||||
)
|
||||
|
||||
scala_test(
|
||||
name = "jfr_events_test",
|
||||
srcs = ["JfrEventsTest.scala"],
|
||||
deps = [
|
||||
"//src/main/scala/net/eagle0/common:jfr_events",
|
||||
],
|
||||
)
|
||||
|
||||
scala_test(
|
||||
name = "more_option_test",
|
||||
srcs = ["MoreOptionTest.scala"],
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
package net.eagle0.common
|
||||
|
||||
import java.nio.file.Files
|
||||
import java.time.Duration
|
||||
|
||||
import scala.jdk.CollectionConverters.*
|
||||
|
||||
import jdk.jfr.consumer.RecordingFile
|
||||
import jdk.jfr.Recording
|
||||
import org.scalatest.flatspec.AnyFlatSpec
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
|
||||
class JfrEventsTest extends AnyFlatSpec with Matchers {
|
||||
"postActionPhase" should "record phase metadata and counts" in {
|
||||
val recordingPath = Files.createTempFile("eagle-post-action-phase", ".jfr")
|
||||
val recording = new Recording()
|
||||
try {
|
||||
recording.enable("net.eagle0.eagle.PostActionPhase").withThreshold(Duration.ZERO)
|
||||
recording.start()
|
||||
|
||||
val result = JfrEvents.postActionPhase("game-123", "client.historyProjection", factionId = 7) { event =>
|
||||
event.actionResultCount = 3
|
||||
event.humanClientCount = 2
|
||||
event.itemCount = 11
|
||||
"result"
|
||||
}
|
||||
|
||||
recording.stop()
|
||||
recording.dump(recordingPath)
|
||||
|
||||
result shouldBe "result"
|
||||
val event = RecordingFile
|
||||
.readAllEvents(recordingPath)
|
||||
.asScala
|
||||
.find(_.getEventType.getName == "net.eagle0.eagle.PostActionPhase")
|
||||
.getOrElse(fail("PostActionPhase event was not recorded"))
|
||||
event.getString("gameId") shouldBe "game-123"
|
||||
event.getString("phase") shouldBe "client.historyProjection"
|
||||
event.getInt("factionId") shouldBe 7
|
||||
event.getInt("actionResultCount") shouldBe 3
|
||||
event.getInt("humanClientCount") shouldBe 2
|
||||
event.getLong("itemCount") shouldBe 11L
|
||||
event.getBoolean("succeeded") shouldBe true
|
||||
event.getStackTrace shouldBe null
|
||||
} finally {
|
||||
recording.close()
|
||||
Files.deleteIfExists(recordingPath): Unit
|
||||
}
|
||||
}
|
||||
|
||||
it should "leave lazy metadata unevaluated when no recording is active" in {
|
||||
var metadataEvaluated = false
|
||||
|
||||
JfrEvents.postActionPhase(
|
||||
{
|
||||
metadataEvaluated = true
|
||||
"game-123"
|
||||
},
|
||||
"unused"
|
||||
)(_ => ())
|
||||
|
||||
metadataEvaluated shouldBe false
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user