Refactored pulse running logic, made it easier to follow and debug

Made the pulse runner use thread-safe collections instead of synchronizing on a thread-unsafe list
This commit is contained in:
vddcore
2022-07-16 07:36:44 +00:00
committed by Ryan
parent c7c97ea176
commit 895444f1a8
8 changed files with 88 additions and 153 deletions
+7 -5
View File
@@ -1,12 +1,10 @@
package gui package gui
import rs09.game.system.SystemLogger
import rs09.game.world.GameWorld import rs09.game.world.GameWorld
import java.awt.BorderLayout import java.awt.BorderLayout
import java.awt.Component import java.awt.Component
import java.awt.Dimension import java.awt.Dimension
import javax.swing.* import javax.swing.*
import javax.swing.border.Border
object ServerMonitor : JFrame("Server Monitor"){ object ServerMonitor : JFrame("Server Monitor"){
@@ -134,7 +132,7 @@ object ServerMonitor : JFrame("Server Monitor"){
fun open() { fun open() {
isVisible = true isVisible = true
Thread() { Thread {
while (true) { while (true) {
SwingUtilities.invokeLater { SwingUtilities.invokeLater {
val removeList = ArrayList<GuiEvent>() val removeList = ArrayList<GuiEvent>()
@@ -159,8 +157,12 @@ object ServerMonitor : JFrame("Server Monitor"){
removeList.add(event) removeList.add(event)
} }
pulseList.text = GameWorld.Pulser.TASKS.toArray().filterNotNull().map { it.javaClass.simpleName + "\n" } pulseList.text = GameWorld.Pulser.currentPulses
.filter { it.replace("\n", "").isNotEmpty() }.sorted().joinToString("") .map { it.javaClass.simpleName + "\n" }
.filter { it.replace("\n", "").isNotEmpty() }
.sorted()
.joinToString("")
eventQueue.removeAll(removeList) eventQueue.removeAll(removeList)
repaint() repaint()
} }
+15 -11
View File
@@ -3,16 +3,9 @@ package rs09
import core.game.system.SystemManager import core.game.system.SystemManager
import core.game.system.SystemState import core.game.system.SystemState
import core.game.system.mysql.SQLManager import core.game.system.mysql.SQLManager
import core.gui.ConsoleFrame
import core.net.NioReactor import core.net.NioReactor
import core.net.amsc.WorldCommunicator
import core.tools.TimeStamp import core.tools.TimeStamp
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.*
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import rs09.game.content.global.GlobalKillCounter;
import rs09.game.ge.GEAutoStock
import rs09.game.ge.GEDB
import rs09.game.system.SystemLogger import rs09.game.system.SystemLogger
import rs09.game.system.config.ServerConfigParser import rs09.game.system.config.ServerConfigParser
import rs09.game.world.GameWorld import rs09.game.world.GameWorld
@@ -97,25 +90,36 @@ object Server {
} }
} }
} }
if (ServerConstants.WATCHDOG_ENABLED) {
GlobalScope.launch { GlobalScope.launch {
delay(20000) delay(20000)
while(running){ while (running) {
if(System.currentTimeMillis() - lastHeartbeat > 7200 && running){ if (System.currentTimeMillis() - lastHeartbeat > 7200 && running) {
SystemLogger.logErr("Triggering reboot due to heartbeat timeout") SystemLogger.logErr("Triggering reboot due to heartbeat timeout")
SystemLogger.logErr("Creating thread dump...") SystemLogger.logErr("Creating thread dump...")
val dump = threadDump(true, true) val dump = threadDump(true, true)
withContext(Dispatchers.IO) {
FileWriter("latestdump.txt").use { FileWriter("latestdump.txt").use {
if (dump != null) {
it.write(dump) it.write(dump)
}
it.flush() it.flush()
it.close() it.close()
} }
if(!SystemManager.isTerminated()) }
if (!SystemManager.isTerminated())
exitProcess(0) exitProcess(0)
} }
delay(625) delay(625)
} }
} }
} }
}
@JvmStatic @JvmStatic
fun heartbeat() { fun heartbeat() {
@@ -134,6 +134,9 @@ class ServerConstants {
@JvmField @JvmField
var RULES_AND_INFO_ENABLED = true var RULES_AND_INFO_ENABLED = true
@JvmField
var WATCHDOG_ENABLED = true
//location names for the ::to command. //location names for the ::to command.
val TELEPORT_DESTINATIONS = arrayOf( val TELEPORT_DESTINATIONS = arrayOf(
arrayOf(Location.create(2974, 4383, 2), "corp", "corporal", "corporeal"), arrayOf(Location.create(2974, 4383, 2), "corp", "corporal", "corporeal"),
@@ -2,9 +2,7 @@ package rs09.game.system.config
import com.moandjiezana.toml.Toml import com.moandjiezana.toml.Toml
import core.game.world.map.Location import core.game.world.map.Location
import core.tools.StringUtils
import core.tools.mysql.Database import core.tools.mysql.Database
import rs09.JSONUtils.Companion.parsePath
import rs09.ServerConstants import rs09.ServerConstants
import rs09.game.system.SystemLogger import rs09.game.system.SystemLogger
import rs09.game.world.GameSettings import rs09.game.world.GameSettings
@@ -121,6 +119,7 @@ object ServerConfigParser {
ServerConstants.BANK_BOOTH_NOTE_ENABLED = data.getBoolean("world.bank_booth_note_enabled", true) ServerConstants.BANK_BOOTH_NOTE_ENABLED = data.getBoolean("world.bank_booth_note_enabled", true)
ServerConstants.BANK_BOOTH_NOTE_UIM = data.getBoolean("world.bank_booth_note_uim", true) ServerConstants.BANK_BOOTH_NOTE_UIM = data.getBoolean("world.bank_booth_note_uim", true)
ServerConstants.DISCORD_GE_WEBHOOK = data.getString("server.discord_webhook", "") ServerConstants.DISCORD_GE_WEBHOOK = data.getString("server.discord_webhook", "")
ServerConstants.WATCHDOG_ENABLED = data.getBoolean("server.watchdog_enabled", true)
} }
@@ -1,99 +1,61 @@
package rs09.game.world package rs09.game.world
import core.game.system.task.Pulse import core.game.system.task.Pulse
import java.util.* import rs09.game.ai.general.GeneralBotCreator
import rs09.game.system.SystemLogger
/** new way of running pulses that multithreads based on core count automatically, should improve performance drastically. import java.util.concurrent.LinkedBlockingQueue
* @author ceik
* @author angle
*/
class PulseRunner { class PulseRunner {
val TASKS = ArrayList<Pulse>() private val pulses = LinkedBlockingQueue<Pulse>()
fun submit(pulse: Pulse){ val currentPulses: Array<Pulse> get() = pulses.toTypedArray()
synchronized(this.TASKS) {
TASKS.add(pulse) fun submit(pulse: Pulse) {
pulses.add(pulse)
}
fun updateAll() {
val pulseCount = pulses.size
for (i in 0..pulseCount) {
val pulse = pulses.take()
val elapsedTime = measure {
try {
if (!pulse.update()) {
pulses.add(pulse)
}
} catch (e: Exception) {
SystemLogger.logErr("Pulse execution error. Stack trace below.")
e.printStackTrace()
}
}
notifyIfTooLong(pulse, elapsedTime)
}
}
private fun measure(logic: () -> Unit): Long {
val startTime = System.currentTimeMillis()
logic()
return System.currentTimeMillis() - startTime
}
private fun notifyIfTooLong(pulse: Pulse, elapsedTime: Long) {
if (elapsedTime >= 100) {
if (pulse is GeneralBotCreator.BotScriptPulse) {
SystemLogger.logWarn("CRITICALLY long bot-script tick - ${pulse.botScript.javaClass.name} took $elapsedTime ms")
} else {
SystemLogger.logWarn("CRITICALLY long running pulse - ${pulse.javaClass.name} took $elapsedTime ms")
}
} else if (elapsedTime >= 30) {
if (pulse is GeneralBotCreator.BotScriptPulse) {
SystemLogger.logWarn("Long bot-script tick - ${pulse.botScript.javaClass.name} took $elapsedTime ms")
} else {
SystemLogger.logWarn("Long running pulse - ${pulse.javaClass.name} took $elapsedTime ms")
}
} }
} }
} }
/*
class PulseeRunner {
val MAXIMUM_NUM_THREADS = 4
val TARGET_PULSES_PER_THREAD = 100
val ThreadPool = Executors.newFixedThreadPool(MAXIMUM_NUM_THREADS - 1) as ThreadPoolExecutor
val TASKS: MutableList<Pulse> = ArrayList()
var cores = Runtime.getRuntime().availableProcessors()
var EXECUTOR = Executors.newSingleThreadScheduledExecutor()
var lastPulse: Pulse? = null
fun init(tickTimeMS: Int) {
EXECUTOR.scheduleAtFixedRate(Runner(), 1200, tickTimeMS.toLong(), TimeUnit.MILLISECONDS)
}
fun submit(pulse: Pulse) {
TASKS.add(pulse)
}
inner class Runner : Runnable {
override fun run() {
val currTime = System.nanoTime()
var pulses: MutableList<Pulse>? = null
pulses = ArrayList(TASKS)
val pulseArray: Array<Any?> = pulses.toTypedArray()
*/
/*var numThreads = 1 + pulseArray.size / TARGET_PULSES_PER_THREAD
if (numThreads > MAXIMUM_NUM_THREADS) numThreads = MAXIMUM_NUM_THREADS
val nowTime = System.nanoTime()
// Execute all the tasks not run on the first core
for (i in 1 until numThreads) {
val pulsesLengthStart = floor(pulseArray.size / numThreads * i.toDouble()).toInt()
var pulsesLengthEnd = floor(pulseArray.size / numThreads * (i + 1).toDouble()).toInt()
if (i + 1 == numThreads) pulsesLengthEnd = pulseArray.size
ThreadPool.execute(PulseThread(pulsesLengthStart, pulsesLengthEnd, pulseArray))
}
// Execute the first core tasks all together just as before
val pulsesLengthStart = floor(pulseArray.size / numThreads.toDouble()).toInt()*//*
for (element in pulseArray) {
val pulse = element as Pulse? ?: continue
try {
if (TASKS.contains(pulse)) {
lastPulse = pulse
if (pulse.update()) {
TASKS.remove(pulse)
}
}
} catch (t: Throwable) {
t.printStackTrace()
pulse.stop()
TASKS.remove(pulse)
}
}
pulses.clear()
}
}
inner class PulseThread(var threadStart: Int, var threadFinish: Int, var pulseArray: Array<Any?>) : Runnable {
override fun run() {
for (i in threadStart until threadFinish) {
val pulse = pulseArray[i] as Pulse? ?: continue
try {
if (TASKS.contains(pulse)) {
lastPulse = pulse
if (pulse.update()) {
TASKS.remove(pulse)
}
}
} catch (t: Throwable) {
t.printStackTrace()
pulse.stop()
TASKS.remove(pulse)
}
}
}
}
}*/
@@ -6,7 +6,6 @@ import core.plugin.CorePluginTypes.Managers
import rs09.Server import rs09.Server
import rs09.ServerConstants import rs09.ServerConstants
import rs09.ServerStore import rs09.ServerStore
import rs09.game.ai.general.GeneralBotCreator
import rs09.game.system.SystemLogger import rs09.game.system.SystemLogger
import rs09.game.world.GameWorld import rs09.game.world.GameWorld
import rs09.game.world.repository.Repository import rs09.game.world.repository.Repository
@@ -30,13 +29,13 @@ class MajorUpdateWorker {
Thread.currentThread().name = "Major Update Worker" Thread.currentThread().name = "Major Update Worker"
started = true started = true
Thread.sleep(600L) Thread.sleep(600L)
while(running){ while (running) {
val start = System.currentTimeMillis() val start = System.currentTimeMillis()
Server.heartbeat() Server.heartbeat()
handleTickActions() handleTickActions()
for (player in Repository.players.filter {!it.isArtificial}) { for (player in Repository.players.filter { !it.isArtificial }) {
if (System.currentTimeMillis() - player.session.lastPing > 20000L) { if (System.currentTimeMillis() - player.session.lastPing > 20000L) {
player?.details?.session?.disconnect() player?.details?.session?.disconnect()
player?.session?.lastPing = Long.MAX_VALUE player?.session?.lastPing = Long.MAX_VALUE
@@ -46,14 +45,14 @@ class MajorUpdateWorker {
} }
//Handle daily restart if enabled //Handle daily restart if enabled
if(sdf.format(Date()).toInt() == 0){ if (sdf.format(Date()).toInt() == 0) {
if(GameWorld.checkDay() == 1) {//monday if (GameWorld.checkDay() == 1) {//monday
ServerStore.clearWeeklyEntries() ServerStore.clearWeeklyEntries()
} }
ServerStore.clearDailyEntries() ServerStore.clearDailyEntries()
if(ServerConstants.DAILY_RESTART ) { if (ServerConstants.DAILY_RESTART) {
Repository.sendNews(colorize("%RSERVER GOING DOWN FOR DAILY RESTART IN 5 MINUTES!")) Repository.sendNews(colorize("%RSERVER GOING DOWN FOR DAILY RESTART IN 5 MINUTES!"))
ServerConstants.DAILY_RESTART = false ServerConstants.DAILY_RESTART = false
submitWorldPulse(object : Pulse(100) { submitWorldPulse(object : Pulse(100) {
@@ -79,40 +78,11 @@ class MajorUpdateWorker {
SystemLogger.logInfo("Update worker stopped.") SystemLogger.logInfo("Update worker stopped.")
} }
fun handleTickActions() { fun handleTickActions(skipPulseUpdate: Boolean = false) {
val rmlist = ArrayList<Pulse>() if (!skipPulseUpdate) {
val list = ArrayList(GameWorld.Pulser.TASKS) GameWorld.Pulser.updateAll()
//run our pulses
for (pulse in list) {
val b = System.currentTimeMillis()
if (pulse == null || pulse.update()) rmlist.add(pulse)
val time = System.currentTimeMillis() - b
if (time >= 100) {
if (pulse is GeneralBotCreator.BotScriptPulse) {
SystemLogger.logWarn("CRITICALLY Long Botscript Tick: ${pulse.botScript.javaClass.name} - $time ms")
} else {
SystemLogger.logWarn("CRITICALLY long running pulse: ${pulse.javaClass.name} - $time ms")
}
} else if (time >= 30) {
if (pulse is GeneralBotCreator.BotScriptPulse) {
SystemLogger.logWarn("Long Botscript Tick: ${pulse.botScript.javaClass.name} - $time ms")
} else {
SystemLogger.logWarn("Long Running Pulse: ${pulse.javaClass.name} - $time ms")
}
}
} }
//remove all null or finished pulses from the list
rmlist.forEach {
synchronized(GameWorld.Pulser.TASKS) {
GameWorld.Pulser.TASKS.remove(it)
}
}
rmlist.clear()
//perform our update sequence where we write masks, etc
try { try {
sequence.start() sequence.start()
sequence.run() sequence.run()
@@ -130,7 +100,7 @@ class MajorUpdateWorker {
} }
fun start() { fun start() {
if(!started){ if (!started) {
running = true running = true
worker.start() worker.start()
} }
+2 -2
View File
@@ -55,10 +55,10 @@ object TestUtils {
} }
} }
fun advanceTicks(amount: Int) { fun advanceTicks(amount: Int, skipPulseUpdates: Boolean = true) {
SystemLogger.logInfo("Advancing ticks by $amount.") SystemLogger.logInfo("Advancing ticks by $amount.")
for(i in 0 until amount) { for(i in 0 until amount) {
GameWorld.majorUpdateWorker.handleTickActions() GameWorld.majorUpdateWorker.handleTickActions(skipPulseUpdates)
} }
} }
} }
@@ -1,9 +1,6 @@
package core package core
import TestUtils import TestUtils
import api.replaceScenery
import core.cache.def.impl.SceneryDefinition
import core.game.interaction.MovementPulse
import core.game.node.scenery.Scenery import core.game.node.scenery.Scenery
import core.game.world.map.Location import core.game.world.map.Location
import core.game.world.map.RegionManager import core.game.world.map.RegionManager
@@ -12,8 +9,6 @@ import org.junit.jupiter.api.Test
import rs09.game.interaction.InteractionListener import rs09.game.interaction.InteractionListener
import rs09.game.interaction.InteractionListeners import rs09.game.interaction.InteractionListeners
import rs09.game.node.entity.skill.gather.GatheringSkillOptionListeners import rs09.game.node.entity.skill.gather.GatheringSkillOptionListeners
import rs09.game.system.SystemLogger
import rs09.game.world.GameWorld
class PathfinderTests { class PathfinderTests {
companion object {init {TestUtils.preTestSetup(); GatheringSkillOptionListeners().defineListeners() }} companion object {init {TestUtils.preTestSetup(); GatheringSkillOptionListeners().defineListeners() }}
@@ -47,7 +42,7 @@ class PathfinderTests {
p.init() p.init()
Assertions.assertEquals(true, InteractionListeners.run(1307, InteractionListener.SCENERY, "chop-down", p, dest!!)) Assertions.assertEquals(true, InteractionListeners.run(1307, InteractionListener.SCENERY, "chop-down", p, dest!!))
TestUtils.advanceTicks(20) TestUtils.advanceTicks(20, false)
Assertions.assertEquals(Location.create(2722, 3475, 0), p.location) Assertions.assertEquals(Location.create(2722, 3475, 0), p.location)
} }
} }