Skip to content

Commit 9907085

Browse files
authored
Merge pull request #15 from integr-dev/develop
### Performance & Optimization * Parallelized the plugin startup and shutdown process, significantly reducing loading times. * Optimized the logging system to be more flexible and reduce impact on startup speed. * Introduced a dedicated server dispatcher to ensure background tasks interact safely with the main server thread. ### Configuration & Management * Centralized all plugin settings into a new configuration system for easier management. * Added a new configuration option to toggle automatic update checks. * Simplified the "wipe" command by removing unnecessary confirmation steps for faster administration. ### Diagnostics & Stability * Added a new "probes" command to help administrators detect and manage memory leaks caused by script hot-reloading. * Implemented an automated background checker to monitor and report potential performance leaks. * Improved the accuracy of startup and shutdown duration logging for better performance tracking. ### Documentation * Updated the README and example code to provide clearer instructions on configuration and resource management.
2 parents 4b7fe0f + 080a214 commit 9907085

26 files changed

Lines changed: 804 additions & 170 deletions

README.md

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ Backbone provides a simple and powerful way to manage your plugin's data and con
170170

171171
#### Resource Pools
172172

173-
A `ResourcePool` is a namespaced container for your resources. It's recommended to create a separate pool for each script or feature set to avoid conflicts.
173+
A `ResourcePool` is a namespaced container for your resources. It's recommended to create a separate pool for each part of your server set to avoid conflicts.
174174

175175
```kotlin
176176
// Create a resource pool for your script's storage
@@ -186,18 +186,17 @@ This will create directories at `storage/mystorage/` and `config/myconfig/` in y
186186

187187
You can manage typed configuration files. Backbone handles the serialization and deserialization of your data classes automatically.
188188

189-
First, define a serializable data class for your configuration:
189+
First, define a data class for your configuration:
190190

191191
```kotlin
192-
@Serializable // Requires the kotlinx.serialization plugin
193192
data class MyConfig(val settingA: String = "default", val settingB: Int = 10)
194193
```
195194

196195
Then, use the `config()` function on your resource pool to get a handler for it:
197196

198197
```kotlin
199198
// Get a handler for a config file named 'settings.yml'
200-
val configHandler = myScriptConfig.config<MyConfig>("settings.yml")
199+
val configHandler = myScriptConfig.config<MyConfig>("settings.yml", MyConfig())
201200

202201
// Load the config file synchronously
203202
configHandler.updateSync()

build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ plugins {
77
}
88

99
group = "net.integr"
10-
version = "1.7.1"
10+
version = "1.8.0"
1111

1212
repositories {
1313
mavenCentral()

src/main/kotlin/net/integr/backbone/Backbone.kt

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import net.integr.backbone.systems.entity.EntityHandler
1818
import net.integr.backbone.systems.event.EventBus
1919
import net.integr.backbone.systems.gui.GuiHandler
2020
import net.integr.backbone.systems.item.ItemHandler
21+
import net.integr.backbone.systems.logger.BackboneLogger
2122
import net.integr.backbone.systems.permission.PermissionNode
2223
import net.integr.backbone.systems.placeholder.PlaceholderGroup
2324
import net.integr.backbone.systems.storage.ResourcePool
@@ -61,6 +62,23 @@ object Backbone {
6162
@ApiStatus.Internal
6263
val CONFIG_POOL = ResourcePool.fromConfig("backbone")
6364

65+
/**
66+
* Backbones main config. **Important:** Do not use this.
67+
* Create a new config instead:
68+
* ```kotlin
69+
* val config = pool.config<MyConfig>("my-config.yaml")
70+
* ```
71+
*
72+
* @since 1.8.0
73+
*/
74+
@get:ApiStatus.Internal
75+
val MAIN_CONFIG by lazy {
76+
CONFIG_POOL.config("backbone.yaml", BackboneConfig())
77+
}
78+
79+
val CONFIG_STATE
80+
get() = MAIN_CONFIG.getState()!!
81+
6482
/**
6583
* Internal script pool.
6684
*
@@ -78,8 +96,10 @@ object Backbone {
7896
*
7997
* @since 1.0.0
8098
*/
81-
@ApiStatus.Internal
82-
val LOGGER = BackboneLogger("backbone")
99+
@get:ApiStatus.Internal
100+
val LOGGER by lazy {
101+
BackboneLogger("backbone", MAIN_CONFIG.getState()!!.loggerCompatibilityMode, pluginInternal)
102+
}
83103

84104
/**
85105
* Backbones root permission. **Important:** Do not use this for your own permission checks.
@@ -116,7 +136,8 @@ object Backbone {
116136
*
117137
* @since 1.0.0
118138
*/
119-
private val pluginInternal: JavaPlugin? by lazy {
139+
@get:ApiStatus.Internal
140+
internal val pluginInternal: JavaPlugin? by lazy {
120141
Utils.tryOrNull { JavaPlugin.getPlugin(BackboneServer::class.java) } // For testing purposes we allow null here
121142
}
122143

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/*
2+
* Copyright © 2026 Integr
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at
6+
* http://www.apache.org/licenses/LICENSE-2.0
7+
* Unless required by applicable law or agreed to in writing, software
8+
* distributed under the License is distributed on an "AS IS" BASIS,
9+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
* See the License for the specific language governing permissions and
11+
* limitations under the License.
12+
*/
13+
14+
package net.integr.backbone
15+
16+
17+
data class BackboneConfig(
18+
val checkForUpdates: Boolean = true,
19+
val loggerCompatibilityMode: Boolean = false
20+
)

src/main/kotlin/net/integr/backbone/BackboneServer.kt

Lines changed: 78 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -13,22 +13,27 @@
1313

1414
package net.integr.backbone
1515

16+
import kotlinx.coroutines.Dispatchers
17+
import kotlinx.coroutines.launch
1618
import kotlinx.coroutines.runBlocking
1719
import net.integr.backbone.commands.BackboneCommand
1820
import net.integr.backbone.events.TickEvent
1921
import net.integr.backbone.systems.bstats.BStatHandler
22+
import net.integr.backbone.systems.diagnostic.ProbeHandler
2023
import net.integr.backbone.systems.entity.EntityHandler
2124
import net.integr.backbone.systems.event.EventBus
2225
import net.integr.backbone.systems.gui.GuiHandler
2326
import net.integr.backbone.systems.hotloader.ScriptEngine
2427
import net.integr.backbone.systems.hotloader.ScriptLinker
2528
import net.integr.backbone.systems.item.ItemHandler
29+
import net.integr.backbone.systems.update.UpdateChecker
2630
import org.bukkit.plugin.java.JavaPlugin
2731
import org.bukkit.scheduler.BukkitTask
2832
import org.jetbrains.annotations.ApiStatus
2933
import java.io.FileDescriptor
3034
import java.io.FileOutputStream
3135
import java.io.PrintStream
36+
import kotlin.time.measureTime
3237

3338
/**
3439
* The main plugin class for the Backbone API.
@@ -37,6 +42,7 @@ import java.io.PrintStream
3742
@ApiStatus.Internal
3843
class BackboneServer : JavaPlugin() {
3944
private var tickTask: BukkitTask? = null
45+
private var probeCheckerTask: BukkitTask? = null
4046
private var beforeOut: PrintStream? = null
4147
private var beforeErr: PrintStream? = null
4248

@@ -45,64 +51,102 @@ class BackboneServer : JavaPlugin() {
4551
* @since 1.0.0
4652
*/
4753
override fun onEnable() {
48-
val out = PrintStream(FileOutputStream(FileDescriptor.out), true)
49-
val err = PrintStream(FileOutputStream(FileDescriptor.err), true)
54+
val timeTaken = measureTime {
55+
val out = PrintStream(FileOutputStream(FileDescriptor.out), true)
56+
val err = PrintStream(FileOutputStream(FileDescriptor.err), true)
5057

51-
beforeOut = System.out
52-
beforeErr = System.err
58+
beforeOut = System.out
59+
beforeErr = System.err
5360

54-
// Bypass papers println intercept
55-
System.setOut(out)
56-
System.setErr(err)
61+
// Bypass papers println intercept
62+
System.setOut(out)
63+
System.setErr(err)
5764

58-
Backbone.SCRIPT_POOL.create()
65+
Backbone.SCRIPT_POOL.create()
5966

60-
runBlocking {
61-
ScriptLinker.compileAndLink()
62-
}
67+
tickTask = Backbone.dispatchMainTimer(0L, 1L) {
68+
EventBus.post(TickEvent())
69+
}
6370

64-
BStatHandler.init()
71+
// Start leak probe checker every 30 seconds (600 ticks)
72+
probeCheckerTask = Backbone.dispatchTimer(1200L, 600L) {
73+
ProbeHandler.check()
74+
}
6575

66-
Backbone.registerListener(GuiHandler)
67-
Backbone.registerListener(ItemHandler)
68-
Backbone.registerListener(EntityHandler)
76+
runBlocking { // Perform initialization tasks in parallel using coroutines to speed up startup time
77+
launch { // Misc initialization
78+
BStatHandler.init()
6979

70-
Backbone.Handler.COMMAND.register(BackboneCommand)
80+
Backbone.registerListener(GuiHandler)
81+
Backbone.registerListener(ItemHandler)
82+
Backbone.registerListener(EntityHandler)
7183

72-
tickTask = Backbone.dispatchMainTimer(0L, 1L) {
73-
EventBus.post(TickEvent())
74-
}
84+
Backbone.Handler.COMMAND.register(BackboneCommand)
85+
}
86+
87+
launch { // Compile and link scripts
88+
val scriptsTimeTaken = measureTime {
89+
ScriptLinker.compileAndLink()
90+
}
91+
92+
Backbone.LOGGER.info("Compiled and linked scripts in ${scriptsTimeTaken.inWholeSeconds}s")
93+
}
94+
95+
launch { // Set up placeholders
96+
setPlaceholders()
7597

76-
Backbone.dispatchMain {
77-
setPlaceholders()
98+
Backbone.PLACEHOLDER_GROUP.registerPlaceholders()
99+
}
78100

79-
Backbone.PLACEHOLDER_GROUP.registerPlaceholders()
101+
launch(Dispatchers.IO) { // Check for updates
102+
if (Backbone.CONFIG_STATE.checkForUpdates) {
103+
UpdateChecker.checkUpdate()
104+
}
105+
}
106+
107+
Backbone.LOGGER.info("Dispatched initialization tasks, waiting for completion...")
108+
}
80109
}
110+
111+
Backbone.LOGGER.info("Initialization tasks completed in ${timeTaken.inWholeSeconds}s, backbone is now enabled!")
81112
}
82113

83114
/**
84115
* Called by bukkit.
85116
* @since 1.0.0
86117
*/
87-
override fun onDisable() {
88-
tickTask?.cancel() // Stop the tick task
118+
override fun onDisable() {
119+
val timeTaken = measureTime {
120+
tickTask?.cancel() // Stop the tick task
121+
probeCheckerTask?.cancel() // Stop the probe checker task
122+
123+
Backbone.PLACEHOLDER_GROUP.unregisterPlaceholders()
89124

90-
Backbone.PLACEHOLDER_GROUP.unregisterPlaceholders()
125+
runBlocking {
126+
launch {
127+
BStatHandler.shutdown()
91128

92-
ScriptEngine.unloadScripts() // Cleanup
129+
Backbone.unregisterListener(GuiHandler)
130+
Backbone.unregisterListener(ItemHandler)
131+
Backbone.unregisterListener(EntityHandler)
93132

94-
BStatHandler.shutdown()
133+
Backbone.Handler.COMMAND.unregister(BackboneCommand)
134+
}
95135

96-
Backbone.unregisterListener(GuiHandler)
97-
Backbone.unregisterListener(ItemHandler)
98-
Backbone.unregisterListener(EntityHandler)
136+
launch {
137+
ScriptEngine.unloadScripts()
138+
}
99139

100-
Backbone.Handler.COMMAND.unregister(BackboneCommand)
140+
Backbone.LOGGER.info("Dispatched shutdown tasks, waiting for completion...")
141+
}
142+
143+
// Restore original System.out and System.err
144+
beforeOut?.let { System.setOut(it) }
145+
beforeErr?.let { System.setErr(it) }
146+
}
101147

148+
Backbone.LOGGER.info("Shutdown tasks completed in ${timeTaken.inWholeMilliseconds}ms, backbone is now disabled!")
102149

103-
// Restore original System.out and System.err
104-
beforeOut?.let { System.setOut(it) }
105-
beforeErr?.let { System.setErr(it) }
106150
}
107151

108152
/**
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/*
2+
* Copyright © 2026 Integr
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at
6+
* http://www.apache.org/licenses/LICENSE-2.0
7+
* Unless required by applicable law or agreed to in writing, software
8+
* distributed under the License is distributed on an "AS IS" BASIS,
9+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
* See the License for the specific language governing permissions and
11+
* limitations under the License.
12+
*/
13+
14+
package net.integr.backbone
15+
16+
import kotlinx.coroutines.CoroutineDispatcher
17+
import kotlinx.coroutines.Dispatchers
18+
import kotlinx.coroutines.Runnable
19+
import kotlinx.coroutines.isActive
20+
import org.bukkit.Bukkit
21+
import kotlin.coroutines.CoroutineContext
22+
23+
/**
24+
* A [CoroutineDispatcher] that dispatches tasks to the main server thread.
25+
*
26+
* This dispatcher checks if the current thread is the primary server thread. If it is, it runs the task immediately.
27+
* Otherwise, it schedules the task to run on the main server thread using Bukkit's scheduler.
28+
*
29+
* @since 1.7.2
30+
*/
31+
class ServerDispatcher : CoroutineDispatcher() {
32+
override fun dispatch(context: CoroutineContext, block: Runnable) {
33+
if (!context.isActive) {
34+
return
35+
}
36+
37+
if (Bukkit.isPrimaryThread()) {
38+
block.run()
39+
} else {
40+
Backbone.SERVER.scheduler.runTask(Backbone.PLUGIN, block)
41+
}
42+
}
43+
}
44+
45+
/**
46+
* Extension function to get an instance of [ServerDispatcher].
47+
*
48+
* @return An instance of [ServerDispatcher] that can be used to dispatch tasks to the main server thread.
49+
* @since 1.7.2
50+
*/
51+
fun Dispatchers.serverDispatcher(): CoroutineDispatcher = ServerDispatcher()

0 commit comments

Comments
 (0)