Improve performance on AutoSavePluginData auto save (#317)

This commit is contained in:
Him188 2021-04-07 22:28:38 +08:00 committed by GitHub
parent 9d052f60d5
commit d50f34e2b7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 176 additions and 71 deletions

View File

@ -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 许可证的约束, 可以在以下链接找到该许可证. * 此源代码的使用受 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. * 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") @file:Suppress("unused", "PropertyName", "PrivatePropertyName")
package net.mamoe.mirai.console.data package net.mamoe.mirai.console.data
import kotlinx.atomicfu.atomic
import kotlinx.coroutines.* import kotlinx.coroutines.*
import net.mamoe.mirai.console.MiraiConsole import net.mamoe.mirai.console.MiraiConsole
import net.mamoe.mirai.console.internal.data.qualifiedNameOrTip 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.ConsoleExperimentalApi
import net.mamoe.mirai.console.util.TimedTask
import net.mamoe.mirai.console.util.launchTimedTask
import net.mamoe.mirai.utils.* import net.mamoe.mirai.utils.*
/** /**
@ -46,6 +47,16 @@ public open class AutoSavePluginData private constructor(
_saveName = saveName _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 @ConsoleExperimentalApi
override fun onInit(owner: PluginDataHolder, storage: PluginDataStorage) { override fun onInit(owner: PluginDataHolder, storage: PluginDataStorage) {
check(owner is AutoSavePluginDataHolder) { "owner must be AutoSavePluginDataHolder for AutoSavePluginData" } check(owner is AutoSavePluginDataHolder) { "owner must be AutoSavePluginDataHolder for AutoSavePluginData" }
@ -57,42 +68,25 @@ public open class AutoSavePluginData private constructor(
this.storage_ = storage this.storage_ = storage
this.owner_ = owner this.owner_ = owner
owner_.coroutineContext[Job]?.invokeOnCompletion { owner_.coroutineContext[Job]?.invokeOnCompletion { save() }
kotlin.runCatching {
doSave() saverTask = owner_.launchTimedTask(
}.onFailure { e -> intervalMillis = autoSaveIntervalMillis_.first,
owner_.coroutineContext[CoroutineExceptionHandler]?.handleException(owner_.coroutineContext, e) coroutineContext = CoroutineName("AutoSavePluginData.saver: ${this::class.qualifiedNameOrTip}")
?.let { return@invokeOnCompletion } ) { save() }
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
)
}
}
if (shouldPerformAutoSaveWheneverChanged()) { if (shouldPerformAutoSaveWheneverChanged()) {
// 定时自动保存, 用于 kts 序列化的对象
owner_.launch(CoroutineName("AutoSavePluginData.timedAutoSave: ${this::class.qualifiedNameOrTip}")) { owner_.launch(CoroutineName("AutoSavePluginData.timedAutoSave: ${this::class.qualifiedNameOrTip}")) {
while (isActive) { while (isActive) {
try { runIgnoreException<CancellationException> { delay(autoSaveIntervalMillis_.last) } ?: return@launch
delay(autoSaveIntervalMillis_.last) // 定时自动保存一次, 用于 kts 序列化的对象 doSave()
} catch (e: CancellationException) {
return@launch
}
withContext(owner_.coroutineContext) {
doSave()
}
} }
} }
} }
} }
@JvmField private var saverTask: TimedTask? = null
@Volatile
internal var lastAutoSaveJob_: Job? = null
@JvmField
internal val currentFirstStartTime_ = atomic(MAGIC_NUMBER_CFST_INIT)
/** /**
* @return `true` , 一段时间后, 即使无属性改变, 也会进行保存. * @return `true` , 一段时间后, 即使无属性改变, 也会进行保存.
@ -102,41 +96,17 @@ public open class AutoSavePluginData private constructor(
return true 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 @ConsoleExperimentalApi
public final override fun onValueChanged(value: Value<*>) { public final override fun onValueChanged(value: Value<*>) {
debuggingLogger1.error { "onValueChanged: $value" } debuggingLogger1.error { "onValueChanged: $value" }
if (::owner_.isInitialized) { saverTask?.setChanged()
lastAutoSaveJob_ = owner_.launch( }
block = updaterBlock,
context = CoroutineName("AutoSavePluginData.passiveAutoSave: ${this::class.qualifiedNameOrTip}") private fun save() {
) kotlin.runCatching {
doSave()
}.onFailure { e ->
logException(e)
} }
} }

View File

@ -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 许可证的约束, 可以在以下链接找到该许可证. * 此源代码的使用受 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. * 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 package net.mamoe.mirai.console.util
import kotlinx.atomicfu.atomic
import kotlinx.atomicfu.loop
import kotlinx.coroutines.* import kotlinx.coroutines.*
import net.mamoe.mirai.console.internal.util.runIgnoreException
import net.mamoe.mirai.utils.currentTimeMillis
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext import kotlin.coroutines.EmptyCoroutineContext
import kotlin.time.seconds
@ConsoleExperimentalApi @ConsoleExperimentalApi
public object CoroutineScopeUtils { 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 @ConsoleExperimentalApi
public class NamedSupervisorJob @JvmOverloads constructor( public class NamedSupervisorJob @JvmOverloads constructor(
private val name: String, private val name: String,

View 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()
}
}