[O] Optimize ranking

pull/88/head
Azalea 2024-11-21 12:10:44 -05:00
parent 2376e511ac
commit e34f0587fe
2 changed files with 76 additions and 28 deletions

View File

@ -5,6 +5,7 @@ import io.ktor.client.*
import io.ktor.client.engine.cio.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.serialization.kotlinx.json.*
import jakarta.persistence.Query
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.apache.tika.Tika
@ -138,7 +139,26 @@ fun Any.long() = when (this) {
}
fun Any.uint32() = long() and 0xFFFFFFFF
fun Any.int() = long().toInt()
val Any.long get() = long()
val Any.int get() = int()
val Any.double get() = when (this) {
is Boolean -> if (this) 1.0 else 0.0
is Number -> toDouble()
is String -> toDouble()
else -> 400 - "Invalid number: $this"
}
operator fun Bool.unaryPlus() = if (this) 1 else 0
val Any?.truthy get() = when (this) {
null -> false
is Bool -> this
is Float -> this != 0f && !isNaN()
is Double -> this != 0.0 && !isNaN()
is Number -> this != 0
is String -> this.isNotBlank()
is Collection<*> -> isNotEmpty()
is Map<*, *> -> isNotEmpty()
else -> true
}
// Collections
fun <T> ls(vararg args: T) = args.toList()
@ -176,7 +196,7 @@ fun Str.ensureEndingSlash() = if (endsWith('/')) this else "$this/"
fun <T: Any> T.logger() = LoggerFactory.getLogger(this::class.java)
// I hate this ;-;
// I hate this ;-; (list destructuring)
operator fun <E> List<E>.component6(): E = get(5)
operator fun <E> List<E>.component7(): E = get(6)
operator fun <E> List<E>.component8(): E = get(7)
@ -185,3 +205,11 @@ operator fun <E> List<E>.component10(): E = get(9)
operator fun <E> List<E>.component11(): E = get(10)
operator fun <E> List<E>.component12(): E = get(11)
operator fun <E> List<E>.component13(): E = get(12)
inline operator fun <reified E> List<Any?>.invoke(i: Int) = get(i) as E
val <F> Pair<F, *>.l get() = component1()
val <S> Pair<*, S>.r get() = component2()
// Database
val Query.exec get() = resultList.map { (it as Array<*>).toList() }

View File

@ -8,7 +8,7 @@ import org.slf4j.LoggerFactory
import kotlin.jvm.optionals.getOrNull
import kotlin.reflect.KClass
abstract class GameApiController<T : IUserData>(name: String, userDataClass: KClass<T>) {
abstract class GameApiController<T : IUserData>(val name: String, userDataClass: KClass<T>) {
val musicMapping = resJson<Map<String, GenericMusicMeta>>("/meta/$name/music.json")
?.mapKeys { it.key.toInt() } ?: emptyMap()
val logger = LoggerFactory.getLogger(javaClass)
@ -31,41 +31,61 @@ abstract class GameApiController<T : IUserData>(name: String, userDataClass: KCl
playlogRepo.findByUserCardExtId(card.extId)
}
private var rankingCache: MutableMap<Long, Pair<Long, List<GenericRankingPlayer>>> = mutableMapOf()
// Pair<time, List<Pair<should_hide, player>>>
private var rankingCache: Pair<Long, List<Pair<Bool, GenericRankingPlayer>>> = 0L to emptyList()
private val rankingCacheDuration = 240_000
@API("ranking")
fun ranking(@RP token: String?): List<GenericRankingPlayer> {
val reqUser = token?.let { us.jwt.auth(it) { u ->
val time = millis()
val tableName = when (name) { "mai2" -> "maimai2"; "chu3" -> "chusan"; else -> name }
// Check if ranking cache needs to be updated
// TODO: pagination
if (time - rankingCache.first > rankingCacheDuration) {
rankingCache = time to us.em.createNativeQuery(
"""
SELECT
u.id,
u.user_name,
u.player_rating,
u.last_play_date,
AVG(p.achievement) / 10000.0 AS acc,
SUM(p.is_full_combo) AS fc,
SUM(p.is_all_perfect) AS ap,
c.ranking_banned or a.opt_out_of_leaderboard AS hide,
a.username
FROM ${tableName}_user_playlog_view p
JOIN ${tableName}_user_data_view u ON p.user_id = u.id
JOIN sega_card c ON u.aime_card_id = c.id
LEFT JOIN aqua_net_user a ON c.net_user_id = a.au_id
GROUP BY p.user_id, u.player_rating
ORDER BY u.player_rating DESC;
"""
).exec.mapIndexed { i, it ->
it[7].truthy to GenericRankingPlayer(
rank = i + 1,
name = it[1].toString(),
rating = it[2]!!.int,
lastSeen = it[3].toString(),
accuracy = it[4]!!.double,
fullCombo = it[5]!!.int,
allPerfect = it[6]!!.int,
username = it[8]?.toString() ?: "user${it[0]}"
)
}
}
val reqUser = token?.let { us.jwt.auth(it) }?.let { u ->
// Optimization: If the user is not banned, we don't need to process user information
if (!u.ghostCard.rankingBanned && !u.cards.any { it.rankingBanned }) null
else u
} }
val cacheKey = reqUser?.auId ?: -1
// Read from cache if we just computed it less than duration ago
rankingCache[cacheKey]?.let { (t, r) ->
if (millis() - t < rankingCacheDuration) return r
}
// TODO: pagination
// Read from cache if we just computed it less than duration ago
// Shadow-ban: Do not show banned cards in the ranking except for the user who owns the card
val players = userDataRepo.findAll().sortedByDescending { it.playerRating }
.filter { (it.card?.rankingBanned != true && it.card?.aquaUser?.optOutOfLeaderboard != true) || it.card?.aquaUser?.let { it == reqUser } ?: false }
return players.filter { it.card != null }.mapIndexed { i, user ->
val card = user.card!!
val plays = playlogRepo.findByUserCardExtId(card.extId)
GenericRankingPlayer(
rank = i + 1,
name = user.userName,
accuracy = plays.acc(),
rating = user.playerRating,
allPerfect = plays.count { it.isAllPerfect },
fullCombo = plays.count { it.isFullCombo },
lastSeen = user.lastPlayDate.toString(),
username = (if (card.isGhost) user.card!!.aquaUser?.username else null) ?: "user${user.card!!.id}"
)
}.also { rankingCache[cacheKey] = millis() to it } // Update cache
return rankingCache.r.filter { !it.l || it.r.username == reqUser?.username }.map { it.r }.also {
logger.info("Ranking computed in ${millis() - time}ms")
}
}
@API("playlog")