Skip to content
This repository has been archived by the owner on Sep 2, 2024. It is now read-only.

Commit

Permalink
lazy trials, detector logic change, improve chat performance
Browse files Browse the repository at this point in the history
  • Loading branch information
homchom committed Dec 27, 2023
1 parent 4b1c23d commit 883c1c0
Show file tree
Hide file tree
Showing 57 changed files with 438 additions and 573 deletions.
35 changes: 35 additions & 0 deletions src/main/java/io/github/homchom/recode/Logging.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
@file:JvmName("Logging")

package io.github.homchom.recode

import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.cancel
import org.slf4j.LoggerFactory
import org.slf4j.event.Level

private val logger = LoggerFactory.getLogger(MOD_ID).apply { isEnabledForLevel(Level.DEBUG) }

fun logInfo(message: String) = logger.info("[$MOD_NAME] $message")

@JvmOverloads
fun logError(message: String, mentionBugReport: Boolean = false) {
val bugString = if (mentionBugReport) {
"\nIf you believe this is a bug, you can report it here: https://github.com/homchom/recode/issues)"
} else ""
logger.error("[$MOD_NAME] $message$bugString")
}

fun logDebug(message: String) = logDebug { message }

inline fun logDebug(lazyMessage: () -> String) {
if (debug) logInfo("[debug] ${lazyMessage()}")
}

/**
* @see cancel
* @see logDebug
*/
fun CoroutineScope.cancelAndLog(message: String) {
logDebug(message)
cancel(message)
}
31 changes: 0 additions & 31 deletions src/main/java/io/github/homchom/recode/Recode.kt
Original file line number Diff line number Diff line change
Expand Up @@ -29,25 +29,14 @@ import net.fabricmc.loader.api.FabricLoader
import net.fabricmc.loader.api.ModContainer
import net.fabricmc.loader.api.metadata.ModMetadata
import net.fabricmc.loader.api.metadata.ModOrigin
import org.slf4j.LoggerFactory
import org.slf4j.event.Level
import java.nio.file.Path
import java.util.*
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import kotlin.random.Random

private val logger = LoggerFactory.getLogger(MOD_ID).apply { isEnabledForLevel(Level.DEBUG) }

object Recode : ModContainer {
val version: String get() = metadata.version.friendlyString

@Deprecated(
"Use kotlin.random.Random instead",
ReplaceWith("Random", "kotlin.random.Random")
)
val random get() = Random

private val container by lazy { FabricLoader.getInstance().getModContainer(MOD_ID).get() }

private var isInitialized = false
Expand Down Expand Up @@ -148,24 +137,4 @@ object LegacyRecode {
CommandHandler.load(dispatcher, registryAccess)
}
}

@JvmStatic
fun info(message: String) = logInfo("[$MOD_NAME] $message")

@JvmStatic
fun error(message: String) = logError("[$MOD_NAME] $message")
}

fun logInfo(message: String) = logger.info("[$MOD_NAME] $message")

@JvmOverloads
fun logError(message: String, mentionBugReport: Boolean = false) {
val bugString = if (mentionBugReport) {
"\nIf you believe this is a bug, you can report it here: https://github.com/homchom/recode/issues)"
} else ""
logger.error("[$MOD_NAME] $message$bugString")
}

fun logDebug(message: String) {
if (debug) logger.info("[$MOD_NAME debug] $message")
}
4 changes: 1 addition & 3 deletions src/main/java/io/github/homchom/recode/event/Validation.kt
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,4 @@ data class SimpleValidated<T> @JvmOverloads constructor(
override var validity: MixedInt = MixedInt(1)
) : Validated, InvokableWrapper<T>

fun <T> Iterable<SimpleValidated<T>>.mapValid() = mapNotNull { if (it.isValid) it.value else null }

fun <T> Array<out SimpleValidated<T>>.mapValid() = mapNotNull { if (it.isValid) it.value else null }
fun <T> Iterable<SimpleValidated<T>>.mapValid() = mapNotNull { if (it.isValid) it.value else null }
95 changes: 45 additions & 50 deletions src/main/java/io/github/homchom/recode/event/trial/DetectorImpl.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@ import io.github.homchom.recode.event.Requester
import io.github.homchom.recode.event.createEvent
import io.github.homchom.recode.ui.sendSystemToast
import io.github.homchom.recode.ui.text.translatedText
import io.github.homchom.recode.util.computeNullable
import io.github.homchom.recode.util.coroutines.cancelAndLog
import io.github.homchom.recode.util.lib.lazyJob
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.SendChannel
Expand Down Expand Up @@ -56,32 +55,9 @@ private open class TrialDetector<T, R : Any>(
) : Detector<T & Any, R> {
private val event = createEvent<R, R> { it }

@OptIn(DelicateCoroutinesApi::class, ExperimentalCoroutinesApi::class)
private val power = Power(
onEnable = {
for (trialIndex in trials.indices) trials[trialIndex].results.listenEach { supplier ->
val successContext = CompletableDeferred<R>(coroutineContext.job)
if (entries.isEmpty()) {
considerEntry(trialIndex, null, supplier, successContext)
}

val iterator = entries.iterator()
for (entry in iterator) {
if (entry.responses.isClosedForSend) {
iterator.remove()
continue
}
considerEntry(trialIndex, entry, supplier, successContext)
}

successContext.invokeOnCompletion { exception ->
if (exception == null) {
val completed = successContext.getCompleted()
logDebug("${this@TrialDetector} succeeded; running with context $completed")
event.run(completed)
}
}
}
listenToTrials()
}
)

Expand Down Expand Up @@ -114,45 +90,64 @@ private open class TrialDetector<T, R : Any>(
}
}

// TODO: this is arguably ioc hell. someone with more experience in concurrency should review this
// TODO: add more comments
@OptIn(DelicateCoroutinesApi::class)
fun considerEntry(
private fun Power.listenToTrials() {
for (trialIndex in trials.indices) trials[trialIndex].results.listenEach { supplier ->
if (entries.isEmpty()) {
considerEntry(trialIndex, null, supplier)
}

val iterator = entries.iterator()
for (entry in iterator) {
if (entry.responses.isClosedForSend) {
iterator.remove()
continue
}
considerEntry(trialIndex, entry, supplier)
}
}
}

@OptIn(DelicateCoroutinesApi::class)
private fun considerEntry(
trialIndex: Int,
entry: DetectEntry<T, R>?,
supplier: Trial.ResultSupplier<T & Any, R>,
successContext: CompletableDeferred<R>,
supplier: Trial.ResultSupplier<T & Any, R>
) {
val entryScope = CoroutineScope(power.coroutineContext + Job(successContext))
val result = computeNullable {
val trialScope = TrialScope(
this@computeNullable,
entryScope,
entry?.hidden ?: false
)
logDebug("trial $trialIndex started for ${debugString(entry?.input, entry?.hidden)}")
supplier.supplyIn(trialScope, entry?.input, entry?.isRequest ?: false)
}
val entryScope = CoroutineScope(power.coroutineContext + lazyJob())

val entryJob = entryScope.launch {
if (result == null) {
entry?.responses?.trySend(null)
return@launch
}

val awaited = result.await()
entry?.responses?.trySend(awaited)
awaited?.let(successContext::complete)
val result = try {
val trialScope = TrialScope(entryScope, entry?.hidden ?: false)
logDebug { "trial $trialIndex started for ${debugString(entry?.input, entry?.hidden)}" }
supplier.supplyIn(trialScope, entry?.input, entry?.isRequest ?: false)
} catch (e: TrialScopeException) {
null
}

entryJob.invokeOnCompletion { exception ->
if (result == null) {
entry?.responses?.trySend(null)
} else result.invokeOnCompletion { exception ->
val state = when (exception) {
is CancellationException -> "cancelled"
null -> "ended"
null -> "ended".also { succeed(result, entry) }
else -> "ended with exception $exception"
}
entryScope.cancelAndLog("trial $trialIndex $state for ${debugString(entry?.input, entry?.hidden)}")
}
}

@OptIn(ExperimentalCoroutinesApi::class)
private fun succeed(result: TrialResult<R>, entry: DetectEntry<T, R>?) {
val awaited = result.getCompleted()
entry?.responses?.trySend(awaited)
if (awaited != null) {
logDebug("$this succeeded; running with context $awaited")
event.run(awaited)
}
}

override fun use(source: Power) = power.use(source)

override fun toString() = "$name detector"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package io.github.homchom.recode.event.trial
import io.github.homchom.recode.Power
import io.github.homchom.recode.event.Listenable
import io.github.homchom.recode.event.Requester
import io.github.homchom.recode.util.unitOrNull
import io.github.homchom.recode.util.lib.unitOrNull
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch

Expand Down
13 changes: 5 additions & 8 deletions src/main/java/io/github/homchom/recode/event/trial/Trial.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package io.github.homchom.recode.event.trial
import io.github.homchom.recode.event.Listenable
import io.github.homchom.recode.event.Validated
import io.github.homchom.recode.event.map
import io.github.homchom.recode.util.computeNullable
import kotlinx.coroutines.*

/**
Expand Down Expand Up @@ -141,16 +140,14 @@ class TrialResult<T : Any> private constructor(private val deferred: Deferred<T?
hidden: Boolean = false
) : this(
scope.async {
computeNullable {
try {
coroutineScope {
val trialScope = TrialScope(
this@computeNullable,
this,
hidden
)
val trialScope = TrialScope(this, hidden)
yield()
trialScope.asyncBlock().also { coroutineContext.cancelChildren() }
}
} catch (e: TrialScopeException) {
null
}
}
)
Expand All @@ -164,7 +161,7 @@ private open class BasedTrial<T, B, R : Any>(
@OptIn(ExperimentalCoroutinesApi::class)
override val results = basis.map { baseContext ->
Trial.ResultSupplier<T & Any, R> { input, isRequest, hidden ->
val result = tests.runTestsIn(this,input ?: defaultInput, baseContext, isRequest)
val result = tests.runTestsIn(this, input ?: defaultInput, baseContext, isRequest)

// handle HideCallbacks
if (baseContext is Validated && hidden != null) result?.invokeOnCompletion { exception ->
Expand Down
45 changes: 11 additions & 34 deletions src/main/java/io/github/homchom/recode/event/trial/TrialScope.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@ package io.github.homchom.recode.event.trial

import io.github.homchom.recode.DEFAULT_TIMEOUT_DURATION
import io.github.homchom.recode.event.Listenable
import io.github.homchom.recode.util.NullableScope
import io.github.homchom.recode.util.unitOrNull
import io.github.homchom.recode.util.lib.unitOrNull
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.ReceiveChannel
Expand All @@ -18,18 +17,17 @@ import kotlin.coroutines.EmptyCoroutineContext
import kotlin.time.Duration

/**
* A [CoroutineScope] and [NullableScope] that a [Trial] executes in.
* A [CoroutineScope] that a [Trial] executes in.
*
* A trial is a test containing one or more suspension points on events; they are useful for detecting an
* occurrence that happens in complex steps. TrialScope includes corresponding DSL functions such as [requireTrue],
* [add], and [test].
* A trial is a test containing one or more suspension points on events; they are useful for detecting
* an occurrence that happens in complex steps. TrialScope includes corresponding DSL functions such as
* [add] and [test].
*
* @param hidden To be used by trials to invalidate "notification-like" intermediate event contexts.
*
* @see trial
*/
class TrialScope @DelicateCoroutinesApi constructor(
private val nullableScope: NullableScope,
val coroutineScope: CoroutineScope,
val hidden: Boolean = false,
) {
Expand Down Expand Up @@ -87,20 +85,6 @@ class TrialScope @DelicateCoroutinesApi constructor(
_rules += rule
}

/**
* Fails the trial if [predicate] is false.
*/
fun requireTrue(predicate: Boolean) {
if (!predicate) fail()
}

/**
* Fails the trial if [predicate] is true.
*/
fun requireFalse(predicate: Boolean) {
if (predicate) fail()
}

/**
* Tests [test] on the first [attempts] values of [channel] until a non-null result is returned.
*
Expand Down Expand Up @@ -136,7 +120,7 @@ class TrialScope @DelicateCoroutinesApi constructor(
crossinline test: (C) -> T?
) {
coroutineScope.launch(coroutineContext, CoroutineStart.UNDISPATCHED) {
channel.consumeEach { test(it)!! }
channel.consumeEach { test(it) ?: throw TrialScopeException() }
}
yield() // fast-fail
}
Expand Down Expand Up @@ -177,21 +161,12 @@ class TrialScope @DelicateCoroutinesApi constructor(
* prepend it with [unaryPlus].
*/
@JvmInline
value class TestResult<T : Any>(val value: T?) {
val passed get() = value != null
}
value class TestResult<T : Any>(val value: T?)

/**
* @return a non-null [TestResult.value] or fails the trial.
*/
operator fun <T : Any> TestResult<T>.unaryPlus() = value ?: fail()

/**
* Fails this trial.
*
* @see NullableScope.fail
*/
fun fail(): Nothing = nullableScope.fail()
operator fun <T : Any> TestResult<T>.unaryPlus() = value ?: throw TrialScopeException()

/**
* @return an instant [TrialResult] with [value]. Use this when a trial does not end asynchronously.
Expand All @@ -211,4 +186,6 @@ class TrialScope @DelicateCoroutinesApi constructor(
* @see unitOrNull
*/
fun Boolean.instantUnitOrNull() = instant(unitOrNull())
}
}

class TrialScopeException : RuntimeException()
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ private val patchRegex = regex {
}

val JoinDFDetector = detector("DF join",
trial(JoinServerEvent, Unit) { _, _ ->
requireFalse(isOnDF) // if already on DF, this is a node switch and should not be tested
requireTrue(mc.currentServer.ipMatchesDF)
trial(JoinServerEvent, Unit) t@{ _, _ ->
if (isOnDF) return@t null // if already on DF, this is a node switch and should not be tested
if (!mc.currentServer.ipMatchesDF) return@t null

val messages = ReceiveChatMessageEvent.add()
val tipMessage = ActiveBoosterInfo.detect(null).map(::Case).addOptional()
Expand Down
Loading

1 comment on commit 883c1c0

@homchom
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also removes compute and computeNullable

Please sign in to comment.