Performance tests

This commit is contained in:
dam 2026-04-29 21:55:41 +03:00
parent aa5f25fed4
commit cdf43d01f8
No known key found for this signature in database
GPG key ID: 4AF4E722399663FB

View file

@ -0,0 +1,186 @@
package content
import TestUtils
import core.game.node.entity.combat.CombatMovementIntents
import core.game.node.entity.combat.equipment.WeaponInterface
import core.game.node.entity.player.Player
import core.game.node.entity.skill.Skills
import core.game.world.GameWorld
import core.game.world.map.Location
import core.game.world.update.UpdateSequence
import core.net.packet.PacketProcessor
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
import java.util.concurrent.TimeUnit
class CombatPerformanceTests {
init {
TestUtils.preTestSetup()
}
@Test
fun serverTickStaysWithinBudgetWithLiveSizedCombatLoad() {
val load = createCombatLoad()
try {
assertEquals(REAL_PLAYER_COUNT, load.players.count { !it.isArtificial })
assertEquals(BOT_PLAYER_COUNT, load.players.count { it.isArtificial })
repeat(WARMUP_TICKS) {
measureCombatTick(load, it)
}
val durations = LongArray(MEASURED_TICKS) { tick ->
measureCombatTick(load, tick + WARMUP_TICKS)
}
val sorted = durations.sorted()
val p90Index = ((sorted.size * 9 + 9) / 10 - 1).coerceIn(0, sorted.lastIndex)
val p90 = sorted[p90Index]
val max = sorted.last()
val durationText = durations.joinToString(prefix = "[", postfix = "]")
assertTrue(
p90 <= HEADROOM_TICK_BUDGET_MILLIS,
"650-player combat p90 tick time should leave room for slower live hardware. " +
"durations=${durationText}ms, p90=${p90}ms, " +
"budget=${HEADROOM_TICK_BUDGET_MILLIS}ms"
)
assertTrue(
max <= LIVE_TICK_BUDGET_MILLIS,
"650-player combat tick should remain under the live 600ms server tick budget. " +
"durations=${durationText}ms, max=${max}ms"
)
} finally {
load.close()
}
}
private fun createCombatLoad(): CombatLoad {
val players = ArrayList<Player>(TOTAL_PLAYER_COUNT)
val closeables = ArrayList<AutoCloseable>(TOTAL_PLAYER_COUNT)
val previousWildPvp = GameWorld.settings!!.wild_pvp_enabled
for (i in 0 until TOTAL_PLAYER_COUNT) {
val player = TestUtils.getMockPlayer("combat_perf_$i", isBot = i >= REAL_PLAYER_COUNT)
players.add(player)
closeables.add(player)
configureMeleePlayer(player)
}
val pairs = players.chunked(2).mapIndexed { index, pair ->
CombatPair(pair[0], pair[1], pairOrigin(index))
}
val load = CombatLoad(players, closeables, pairs, previousWildPvp)
GameWorld.settings!!.wild_pvp_enabled = true
load.resetPairPositionsAndMovement(0)
for (pair in pairs) {
pair.first.attack(pair.second)
pair.second.attack(pair.first)
}
return load
}
private fun measureCombatTick(load: CombatLoad, tick: Int): Long {
PacketProcessor.queue.clear()
load.resetPairPositionsAndMovement(tick)
load.requestAllCombatMovement()
assertEquals(
TOTAL_PLAYER_COUNT,
CombatMovementIntents.pendingCount(),
"The performance fixture should exercise one combat movement intent per loaded player."
)
val start = System.nanoTime()
GameWorld.majorUpdateWorker.handleTickActions(false)
return TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start)
}
private fun configureMeleePlayer(player: Player) {
player.properties.attackStyle = WeaponInterface.AttackStyle(
WeaponInterface.STYLE_AGGRESSIVE,
WeaponInterface.BONUS_CRUSH
)
player.properties.combatPulse.updateStyle()
player.properties.combatLevel = 126
player.skills.setStaticLevel(Skills.HITPOINTS, 10_000)
player.skills.lifepoints = 10_000
player.settings.runEnergy = 100.0
player.settings.setRunToggled(true)
player.skullManager.isWilderness = true
player.skullManager.level = 50
}
private data class CombatPair(
val first: Player,
val second: Player,
val origin: Location
)
private class CombatLoad(
val players: List<Player>,
private val closeables: List<AutoCloseable>,
private val pairs: List<CombatPair>,
private val previousWildPvp: Boolean
) : AutoCloseable {
fun resetPairPositionsAndMovement(tick: Int) {
val direction = if (tick % 2 == 0) 1 else -1
for (pair in pairs) {
val firstLocation = pair.origin
val secondLocation = pair.origin.transform(1, 0, 0)
place(pair.first, firstLocation)
place(pair.second, secondLocation)
queueRun(pair.first, firstLocation.transform(-8 * direction, 0, 0))
queueRun(pair.second, secondLocation.transform(8 * direction, 0, 0))
}
}
fun requestAllCombatMovement() {
CombatMovementIntents.clear()
for (pair in pairs) {
CombatMovementIntents.request(pair.first, pair.second)
CombatMovementIntents.request(pair.second, pair.first)
}
}
override fun close() {
CombatMovementIntents.clear()
for (closeable in closeables.asReversed()) {
closeable.close()
}
GameWorld.Pulser.updateAll()
UpdateSequence.renderablePlayers.sync()
PacketProcessor.queue.clear()
GameWorld.settings!!.wild_pvp_enabled = previousWildPvp
}
private fun place(player: Player, location: Location) {
player.location = location
player.walkingQueue.reset()
}
private fun queueRun(player: Player, destination: Location) {
player.walkingQueue.reset(true)
player.walkingQueue.addPath(destination.x, destination.y)
}
}
private companion object {
const val REAL_PLAYER_COUNT = 150
const val BOT_PLAYER_COUNT = 500
const val TOTAL_PLAYER_COUNT = REAL_PLAYER_COUNT + BOT_PLAYER_COUNT
const val WARMUP_TICKS = 3
const val MEASURED_TICKS = 10
const val LIVE_TICK_BUDGET_MILLIS = 600L
const val HEADROOM_TICK_BUDGET_MILLIS = 350L
fun pairOrigin(index: Int): Location {
val columns = 25
val column = index % columns
val row = index / columns
return Location.create(3200 + column * 12, 3600 + row * 6, 0)
}
}
}