mirror of
https://github.com/mamoe/mirai.git
synced 2025-01-25 15:40:28 +08:00
Improve performance on AutoSavePluginData auto save (#317)
This commit is contained in:
parent
9d052f60d5
commit
d50f34e2b7
@ -1,22 +1,23 @@
|
||||
/*
|
||||
* Copyright 2019-2020 Mamoe Technologies and contributors.
|
||||
* Copyright 2019-2021 Mamoe Technologies and contributors.
|
||||
*
|
||||
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
|
||||
* Use of this source code is governed by the GNU AFFERO GENERAL PUBLIC LICENSE version 3 license that can be found through the following link.
|
||||
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
|
||||
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
|
||||
*
|
||||
* https://github.com/mamoe/mirai/blob/master/LICENSE
|
||||
* https://github.com/mamoe/mirai/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
@file:Suppress("unused", "PropertyName", "PrivatePropertyName")
|
||||
|
||||
package net.mamoe.mirai.console.data
|
||||
|
||||
import kotlinx.atomicfu.atomic
|
||||
import kotlinx.coroutines.*
|
||||
import net.mamoe.mirai.console.MiraiConsole
|
||||
import net.mamoe.mirai.console.internal.data.qualifiedNameOrTip
|
||||
import net.mamoe.mirai.console.internal.plugin.updateWhen
|
||||
import net.mamoe.mirai.console.internal.util.runIgnoreException
|
||||
import net.mamoe.mirai.console.util.ConsoleExperimentalApi
|
||||
import net.mamoe.mirai.console.util.TimedTask
|
||||
import net.mamoe.mirai.console.util.launchTimedTask
|
||||
import net.mamoe.mirai.utils.*
|
||||
|
||||
/**
|
||||
@ -46,6 +47,16 @@ public open class AutoSavePluginData private constructor(
|
||||
_saveName = saveName
|
||||
}
|
||||
|
||||
private fun logException(e: Throwable) {
|
||||
owner_.coroutineContext[CoroutineExceptionHandler]?.handleException(owner_.coroutineContext, e)
|
||||
?.let { return }
|
||||
MiraiConsole.mainLogger.error(
|
||||
"An exception occurred when saving config ${this@AutoSavePluginData::class.qualifiedNameOrTip} " +
|
||||
"but CoroutineExceptionHandler not found in PluginDataHolder.coroutineContext for ${owner_::class.qualifiedNameOrTip}",
|
||||
e
|
||||
)
|
||||
}
|
||||
|
||||
@ConsoleExperimentalApi
|
||||
override fun onInit(owner: PluginDataHolder, storage: PluginDataStorage) {
|
||||
check(owner is AutoSavePluginDataHolder) { "owner must be AutoSavePluginDataHolder for AutoSavePluginData" }
|
||||
@ -57,42 +68,25 @@ public open class AutoSavePluginData private constructor(
|
||||
this.storage_ = storage
|
||||
this.owner_ = owner
|
||||
|
||||
owner_.coroutineContext[Job]?.invokeOnCompletion {
|
||||
kotlin.runCatching {
|
||||
doSave()
|
||||
}.onFailure { e ->
|
||||
owner_.coroutineContext[CoroutineExceptionHandler]?.handleException(owner_.coroutineContext, e)
|
||||
?.let { return@invokeOnCompletion }
|
||||
MiraiConsole.mainLogger.error(
|
||||
"An exception occurred when saving config ${this@AutoSavePluginData::class.qualifiedNameOrTip} " +
|
||||
"but CoroutineExceptionHandler not found in PluginDataHolder.coroutineContext for ${owner::class.qualifiedNameOrTip}",
|
||||
e
|
||||
)
|
||||
}
|
||||
}
|
||||
owner_.coroutineContext[Job]?.invokeOnCompletion { save() }
|
||||
|
||||
saverTask = owner_.launchTimedTask(
|
||||
intervalMillis = autoSaveIntervalMillis_.first,
|
||||
coroutineContext = CoroutineName("AutoSavePluginData.saver: ${this::class.qualifiedNameOrTip}")
|
||||
) { save() }
|
||||
|
||||
if (shouldPerformAutoSaveWheneverChanged()) {
|
||||
// 定时自动保存, 用于 kts 序列化的对象
|
||||
owner_.launch(CoroutineName("AutoSavePluginData.timedAutoSave: ${this::class.qualifiedNameOrTip}")) {
|
||||
while (isActive) {
|
||||
try {
|
||||
delay(autoSaveIntervalMillis_.last) // 定时自动保存一次, 用于 kts 序列化的对象
|
||||
} catch (e: CancellationException) {
|
||||
return@launch
|
||||
}
|
||||
withContext(owner_.coroutineContext) {
|
||||
doSave()
|
||||
}
|
||||
runIgnoreException<CancellationException> { delay(autoSaveIntervalMillis_.last) } ?: return@launch
|
||||
doSave()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@JvmField
|
||||
@Volatile
|
||||
internal var lastAutoSaveJob_: Job? = null
|
||||
|
||||
@JvmField
|
||||
internal val currentFirstStartTime_ = atomic(MAGIC_NUMBER_CFST_INIT)
|
||||
private var saverTask: TimedTask? = null
|
||||
|
||||
/**
|
||||
* @return `true` 时, 一段时间后, 即使无属性改变, 也会进行保存.
|
||||
@ -102,41 +96,17 @@ public open class AutoSavePluginData private constructor(
|
||||
return true
|
||||
}
|
||||
|
||||
private val updaterBlock: suspend CoroutineScope.() -> Unit = l@{
|
||||
if (::storage_.isInitialized) {
|
||||
currentFirstStartTime_.updateWhen({ it == MAGIC_NUMBER_CFST_INIT }, { currentTimeMillis() })
|
||||
try {
|
||||
delay(autoSaveIntervalMillis_.first.coerceAtLeast(1000)) // for safety
|
||||
} catch (e: CancellationException) {
|
||||
return@l
|
||||
}
|
||||
|
||||
if (lastAutoSaveJob_ == this.coroutineContext[Job]) {
|
||||
|
||||
withContext(owner_.coroutineContext) {
|
||||
doSave()
|
||||
}
|
||||
} else {
|
||||
if (currentFirstStartTime_.updateWhen(
|
||||
{ it != MAGIC_NUMBER_CFST_INIT && currentTimeMillis() - it >= autoSaveIntervalMillis_.last },
|
||||
{ MAGIC_NUMBER_CFST_INIT })
|
||||
) {
|
||||
withContext(owner_.coroutineContext) {
|
||||
doSave()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ConsoleExperimentalApi
|
||||
public final override fun onValueChanged(value: Value<*>) {
|
||||
debuggingLogger1.error { "onValueChanged: $value" }
|
||||
if (::owner_.isInitialized) {
|
||||
lastAutoSaveJob_ = owner_.launch(
|
||||
block = updaterBlock,
|
||||
context = CoroutineName("AutoSavePluginData.passiveAutoSave: ${this::class.qualifiedNameOrTip}")
|
||||
)
|
||||
saverTask?.setChanged()
|
||||
}
|
||||
|
||||
private fun save() {
|
||||
kotlin.runCatching {
|
||||
doSave()
|
||||
}.onFailure { e ->
|
||||
logException(e)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,19 +1,22 @@
|
||||
/*
|
||||
* Copyright 2019-2020 Mamoe Technologies and contributors.
|
||||
* Copyright 2019-2021 Mamoe Technologies and contributors.
|
||||
*
|
||||
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
|
||||
* Use of this source code is governed by the GNU AFFERO GENERAL PUBLIC LICENSE version 3 license that can be found through the following link.
|
||||
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
|
||||
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
|
||||
*
|
||||
* https://github.com/mamoe/mirai/blob/master/LICENSE
|
||||
* https://github.com/mamoe/mirai/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
@file:JvmName("CoroutineScopeUtils")
|
||||
|
||||
package net.mamoe.mirai.console.util
|
||||
|
||||
import kotlinx.atomicfu.atomic
|
||||
import kotlinx.atomicfu.loop
|
||||
import kotlinx.coroutines.*
|
||||
import net.mamoe.mirai.console.internal.util.runIgnoreException
|
||||
import net.mamoe.mirai.utils.currentTimeMillis
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.coroutines.EmptyCoroutineContext
|
||||
import kotlin.time.seconds
|
||||
|
||||
@ConsoleExperimentalApi
|
||||
public object CoroutineScopeUtils {
|
||||
@ -42,6 +45,49 @@ public object CoroutineScopeUtils {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs `action` every `intervalMillis` since each time [setChanged] is called, ignoring subsequent calls during the interval.
|
||||
*/
|
||||
internal class TimedTask(
|
||||
scope: CoroutineScope,
|
||||
coroutineContext: CoroutineContext = EmptyCoroutineContext,
|
||||
intervalMillis: Long,
|
||||
action: suspend CoroutineScope.() -> Unit,
|
||||
) {
|
||||
companion object {
|
||||
private const val UNCHANGED = 0L
|
||||
}
|
||||
|
||||
private val lastChangedTime = atomic(UNCHANGED)
|
||||
|
||||
fun setChanged() {
|
||||
lastChangedTime.value = currentTimeMillis()
|
||||
}
|
||||
|
||||
val job: Job = scope.launch(coroutineContext) {
|
||||
// `delay` always checks for cancellation
|
||||
lastChangedTime.loop { last ->
|
||||
val current = currentTimeMillis()
|
||||
if (last == UNCHANGED) {
|
||||
runIgnoreException<CancellationException> {
|
||||
delay(3.seconds) // accuracy not necessary
|
||||
} ?: return@launch
|
||||
return@loop
|
||||
}
|
||||
if (current - last > intervalMillis) {
|
||||
if (!lastChangedTime.compareAndSet(last, UNCHANGED)) return@loop
|
||||
action()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun CoroutineScope.launchTimedTask(
|
||||
intervalMillis: Long,
|
||||
coroutineContext: CoroutineContext = EmptyCoroutineContext,
|
||||
action: suspend CoroutineScope.() -> Unit,
|
||||
) = TimedTask(this, coroutineContext, intervalMillis, action)
|
||||
|
||||
@ConsoleExperimentalApi
|
||||
public class NamedSupervisorJob @JvmOverloads constructor(
|
||||
private val name: String,
|
||||
|
89
backend/mirai-console/test/util/TestCoroutineUtils.kt
Normal file
89
backend/mirai-console/test/util/TestCoroutineUtils.kt
Normal file
@ -0,0 +1,89 @@
|
||||
/*
|
||||
* Copyright 2019-2021 Mamoe Technologies and contributors.
|
||||
*
|
||||
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
|
||||
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
|
||||
*
|
||||
* https://github.com/mamoe/mirai/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
|
||||
package net.mamoe.mirai.console.util
|
||||
|
||||
import kotlinx.coroutines.*
|
||||
import org.junit.jupiter.api.Test
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.time.seconds
|
||||
|
||||
internal class TestCoroutineUtils {
|
||||
|
||||
@Test
|
||||
fun `test launchTimedTask 0 time`() = runBlocking {
|
||||
val scope = CoroutineScope(SupervisorJob())
|
||||
|
||||
val result = withTimeoutOrNull(6000) {
|
||||
suspendCancellableCoroutine<Unit> { cont ->
|
||||
scope.launchTimedTask(5.seconds.toLongMilliseconds()) {
|
||||
cont.resume(Unit)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assertEquals(null, result)
|
||||
scope.cancel()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test launchTimedTask finishes 1 time`() = runBlocking {
|
||||
val scope = CoroutineScope(SupervisorJob())
|
||||
|
||||
withTimeout(4000) {
|
||||
suspendCancellableCoroutine<Unit> { cont ->
|
||||
val task = scope.launchTimedTask(3.seconds.toLongMilliseconds()) {
|
||||
cont.resume(Unit)
|
||||
}
|
||||
task.setChanged()
|
||||
}
|
||||
}
|
||||
|
||||
scope.cancel()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test launchTimedTask finishes multiple times`() = runBlocking {
|
||||
val scope = CoroutineScope(SupervisorJob())
|
||||
|
||||
withTimeout(10000) {
|
||||
suspendCancellableCoroutine<Unit> { cont ->
|
||||
val task = scope.launchTimedTask(3.seconds.toLongMilliseconds()) {
|
||||
cont.resume(Unit)
|
||||
}
|
||||
task.setChanged()
|
||||
launch {
|
||||
delay(4000)
|
||||
task.setChanged()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
scope.cancel()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test launchTimedTask interval less than delay`() = runBlocking {
|
||||
val scope = CoroutineScope(SupervisorJob())
|
||||
|
||||
withTimeout(5000) {
|
||||
suspendCancellableCoroutine<Unit> { cont ->
|
||||
val task = scope.launchTimedTask(1.seconds.toLongMilliseconds()) {
|
||||
cont.resume(Unit)
|
||||
}
|
||||
task.setChanged()
|
||||
}
|
||||
}
|
||||
|
||||
scope.cancel()
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue
Block a user