diff options
| author | Sho Sakuma <me@m1sk9.dev> | 2026-08-03 00:48:56 +0900 |
|---|---|---|
| committer | Sho Sakuma <me@m1sk9.dev> | 2026-08-05 00:42:24 +0900 |
| commit | c7e23c73b4c5863242646d61c1d7d3296bdfb4ba (patch) | |
| tree | f45398fd6d1da767ab86b1466c26980a93b05d48 | |
| parent | 8529d652372c5c85e9624e23c4fc1a67bebb00b2 (diff) | |
| download | LunaticChat-c7e23c73b4c5863242646d61c1d7d3296bdfb4ba.tar.gz LunaticChat-c7e23c73b4c5863242646d61c1d7d3296bdfb4ba.tar.bz2 LunaticChat-c7e23c73b4c5863242646d61c1d7d3296bdfb4ba.zip | |
perf: coalesce channel writes instead of rewriting the file per change
Every channel mutation built a full snapshot - copying the channel map,
every member list, and stringifying a UUID per active player - and then
queued a task that pretty-printed and rewrote all of channels.json. There
was no debounce, unlike the settings storage, so a fifty-player join wave
meant fifty snapshots and fifty whole-file writes. Joins and quits both go
through it via setPlayerChannel, and so does the self-healing branch of
getPlayerChannelContext, which sits on the channel-chat message path.
ChannelStorage now debounces like the other two storages, and takes the
snapshot as a supplier so it is built once when the write runs rather than
once per queued change. saveToStorage and saveToDisk had the same six-line
snapshot construction; that is now one private function.
The trade-off is the same one the settings storage already makes: a crash
within the debounce window loses the last few seconds of channel state.
Shutdown still writes synchronously.
Co-Authored-By: Claude <noreply@anthropic.com>
3 files changed, 24 insertions, 25 deletions
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/ServiceInitializer.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/ServiceInitializer.kt index fdfb9fa..9729f55 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/ServiceInitializer.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/ServiceInitializer.kt @@ -221,7 +221,7 @@ class ServiceInitializer( val storage = ChannelStorage( channelsFile = channelsFile, - plugin = plugin, + saver = DebouncedSaver(plugin), logger = logger, ) diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/channel/ChannelManager.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/channel/ChannelManager.kt index 07b5662..7b41f75 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/channel/ChannelManager.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/channel/ChannelManager.kt @@ -434,28 +434,27 @@ class ChannelManager( * Saves the current state of channels and members to storage asynchronously. */ private fun saveToStorage() { - val data = - ChannelData( - channels = channelsCache.toMap(), - members = membersCache.mapValues { it.value.toList() }, - activeChannels = activeChannels.mapKeys { it.key.toString() }, - ) - storage.queueAsyncSave(data) + storage.queueAsyncSave(::snapshot) logger.fine("${channelsCache.size} channels queued for saving to storage.") } /** + * A point-in-time copy of everything persisted. Built inside the write rather than at each + * call site, so a burst of changes copies the caches once. + */ + private fun snapshot(): ChannelData = + ChannelData( + channels = channelsCache.toMap(), + members = membersCache.mapValues { it.value.toList() }, + activeChannels = activeChannels.mapKeys { it.key.toString() }, + ) + + /** * Saves the current state of channels and members to storage synchronously. * Should only br called during server shutdown. */ fun saveToDisk() { - val data = - ChannelData( - channels = channelsCache.toMap(), - members = membersCache.mapValues { it.value.toList() }, - activeChannels = activeChannels.mapKeys { it.key.toString() }, - ) - storage.saveToDisk(data) + storage.saveToDisk(snapshot()) } /** diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/channel/ChannelStorage.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/channel/ChannelStorage.kt index 7a0c3c8..832f8f3 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/channel/ChannelStorage.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/channel/ChannelStorage.kt @@ -3,8 +3,8 @@ package dev.m1sk9.lunaticChat.paper.chat.channel import dev.m1sk9.lunaticChat.engine.chat.channel.ChannelData import dev.m1sk9.lunaticChat.engine.exception.ChannelStorageLoadException import dev.m1sk9.lunaticChat.engine.exception.ChannelStorageSaveException +import dev.m1sk9.lunaticChat.paper.DebouncedSaver import kotlinx.serialization.json.Json -import org.bukkit.plugin.java.JavaPlugin import java.nio.file.Path import java.util.logging.Logger import kotlin.io.path.bufferedReader @@ -15,12 +15,12 @@ import kotlin.io.path.writeText * Manages the storage of channel data on disk. * * @property channelsFile The path to the file where channel data is stored. - * @property plugin The JavaPlugin instance for accessing plugin resources. + * @property saver Coalesces bursts of save requests into one asynchronous write. * @property logger The logger for logging messages. */ class ChannelStorage( private val channelsFile: Path, - private val plugin: JavaPlugin, + private val saver: DebouncedSaver, private val logger: Logger, ) { private val json = @@ -78,18 +78,18 @@ class ChannelStorage( } /** - * Queues an asynchronous save of channel data to disk. + * Queues a debounced asynchronous save of channel data to disk. * - * @param data The ChannelData to save. - * @throws ChannelStorageSaveException if there is an error saving the data. + * @param data Supplies the channel data to write. It is called when the write runs rather + * than when it is queued, so a burst of channel changes costs one snapshot and one file + * write instead of one of each per change. */ - fun queueAsyncSave(data: ChannelData) { - plugin.server.asyncScheduler.runNow(plugin) { + fun queueAsyncSave(data: () -> ChannelData) { + saver.request { try { - saveToDisk(data) + saveToDisk(data()) } catch (e: ChannelStorageSaveException) { logger.severe("Error saving channel data asynchronously: ${e.message}") - e.printStackTrace() } } } |
