diff --git a/src/backend-api/src/main/kotlin/org/icpclive/api/TimeLineRunInfo.kt b/src/backend-api/src/main/kotlin/org/icpclive/api/TimeLineRunInfo.kt new file mode 100644 index 000000000..b36725536 --- /dev/null +++ b/src/backend-api/src/main/kotlin/org/icpclive/api/TimeLineRunInfo.kt @@ -0,0 +1,26 @@ +package org.icpclive.api + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import org.icpclive.cds.api.ProblemId +import org.icpclive.cds.util.serializers.DurationInMillisecondsSerializer +import kotlin.time.Duration + +@Serializable +sealed class TimeLineRunInfo { + @Serializable + @SerialName("ICPC") + public data class ICPC( + @Serializable(with = DurationInMillisecondsSerializer::class) val time: Duration, + val problemId: ProblemId, + val isAccepted: Boolean, + val shortName: String) : TimeLineRunInfo() + + @Serializable + @SerialName("IOI") + public data class IOI(@Serializable(with = DurationInMillisecondsSerializer::class) val time: Duration, val problemId: ProblemId, val score: Double) : TimeLineRunInfo() + + @Serializable + @SerialName("IN_PROGRESS") + public data class InProgress(@Serializable(with = DurationInMillisecondsSerializer::class) val time: Duration, val problemId: ProblemId) : TimeLineRunInfo() +} diff --git a/src/backend/src/main/kotlin/org/icpclive/data/DataBus.kt b/src/backend/src/main/kotlin/org/icpclive/data/DataBus.kt index 682c7cfe1..ecc1da8bc 100644 --- a/src/backend/src/main/kotlin/org/icpclive/data/DataBus.kt +++ b/src/backend/src/main/kotlin/org/icpclive/data/DataBus.kt @@ -17,6 +17,7 @@ object DataBus { val mainScreenFlow = CompletableDeferred>() val queueFlow = CompletableDeferred>() val externalRunsFlow = CompletableDeferred>>() + val timelineFlow = CompletableDeferred>>>() // flow of run ids that need to be braking news val queueFeaturedRunsFlow = CompletableDeferred>() diff --git a/src/backend/src/main/kotlin/org/icpclive/overlay/Routing.kt b/src/backend/src/main/kotlin/org/icpclive/overlay/Routing.kt index 6e93e9974..b322b4571 100644 --- a/src/backend/src/main/kotlin/org/icpclive/overlay/Routing.kt +++ b/src/backend/src/main/kotlin/org/icpclive/overlay/Routing.kt @@ -17,9 +17,15 @@ import org.icpclive.data.currentContestInfoFlow import org.icpclive.util.sendJsonFlow import kotlin.time.Duration -inline fun Route.flowEndpoint(name: String, crossinline dataProvider: suspend () -> Flow) { - webSocket(name) { sendJsonFlow(dataProvider()) } - get(name) { call.respond(dataProvider().first()) } +inline fun Route.flowEndpoint(name: String, crossinline dataProvider: suspend (ApplicationCall) -> Flow?) { + webSocket(name) { + val flow = dataProvider(call) ?: return@webSocket + sendJsonFlow(flow) + } + get(name) { + val result = dataProvider(call)?.first() ?: return@get + call.respond(result) + } } private inline fun Route.setUpScoreboard(crossinline getter: suspend DataBus.(OptimismLevel) -> Flow) { @@ -32,29 +38,18 @@ fun Route.configureOverlayRouting() { flowEndpoint("/mainScreen") { DataBus.mainScreenFlow.await() } flowEndpoint("/contestInfo") { DataBus.currentContestInfoFlow() } flowEndpoint("/runs") { DataBus.contestStateFlow.await().map { it.runsAfterEvent.values.sortedBy { it.time } } } - webSocket("/teamRuns/{id}") { + flowEndpoint("/teamRuns/{id}") { call -> val teamIdStr = call.parameters["id"] if (teamIdStr.isNullOrBlank()) { - close(CloseReason(CloseReason.Codes.CANNOT_ACCEPT, "Invalid team id")) - return@webSocket + call.respond(HttpStatusCode.BadRequest, "Invalid team id") + null + } else { + val teamId = teamIdStr.toTeamId() + DataBus.timelineFlow.await() + .map { it[teamId] } + .distinctUntilChanged { a, b -> a === b } + .map { it ?: emptyList() } } - val teamId = teamIdStr.toTeamId() - val acceptedProblems = mutableSetOf() - val allRuns = mutableMapOf() - DataBus.contestStateFlow.await().first().runsAfterEvent.values - .filter { teamId == it.teamId && it.time != Duration.ZERO } - .forEach { allRuns[it.id] = it } - sendJsonFlow(DataBus.contestStateFlow.await() - .mapNotNull { (it.lastEvent as? RunUpdate)?.newInfo } - .runningFold(allRuns.toPersistentMap()) { acc, it -> - if (it.teamId == teamId) acc.put(it.id, it) else if (it.id in acc) acc.remove(it.id) else acc - } - .distinctUntilChanged { a, b -> a === b } - .map { runs -> runs.values.sortedBy { it.time } } - .map { runs -> - acceptedProblems.clear() - runs.mapNotNull { info -> TimeLineRunInfo.fromRunInfo(info, acceptedProblems) } - }) } flowEndpoint("/queue") { DataBus.queueFlow.await() } flowEndpoint("/statistics") { DataBus.statisticFlow.await() } diff --git a/src/backend/src/main/kotlin/org/icpclive/service/ServiceRoot.kt b/src/backend/src/main/kotlin/org/icpclive/service/ServiceRoot.kt index 2902cfb46..928015ec0 100644 --- a/src/backend/src/main/kotlin/org/icpclive/service/ServiceRoot.kt +++ b/src/backend/src/main/kotlin/org/icpclive/service/ServiceRoot.kt @@ -53,5 +53,6 @@ fun CoroutineScope.launchServices(loader: Flow) { launchService(ExternalRunsService()) launchService(TeamSpotlightService(teamInteresting = teamInterestingFlow)) launchService(RegularLoggingService()) + launchService(TimelineService()) started.update { it - 1 } } diff --git a/src/backend/src/main/kotlin/org/icpclive/service/TimelineService.kt b/src/backend/src/main/kotlin/org/icpclive/service/TimelineService.kt new file mode 100644 index 000000000..65b2db2fc --- /dev/null +++ b/src/backend/src/main/kotlin/org/icpclive/service/TimelineService.kt @@ -0,0 +1,66 @@ +package org.icpclive.service + +import kotlinx.collections.immutable.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.* +import org.icpclive.api.TimeLineRunInfo +import org.icpclive.cds.* +import org.icpclive.cds.adapters.* +import org.icpclive.cds.api.* +import org.icpclive.api.TimeLineRunInfo.* +import org.icpclive.cds.scoreboard.* +import org.icpclive.cds.utils.TeamRunsStorage +import org.icpclive.data.DataBus + +internal class TimelineService : Service { + override fun CoroutineScope.runOn(flow: Flow) { + DataBus.timelineFlow.complete(flow.map { it.state.lastEvent }.timelineFlow().stateIn(this, SharingStarted.Eagerly, emptyMap())) + } + + private fun Flow.timelineFlow() = flow { + var rows = persistentMapOf>() + val runsByTeamId = TeamRunsStorage() + contestState().collect { state -> + for (team in runsByTeamId.applyEvent(state)) { + val newRow = runsByTeamId.getRuns(team).toTimeLine() + val oldRow = rows[team] + if (newRow != oldRow) { // optimization: avoid identity change, if no real change + rows = rows.put(team, newRow) + } + } + emit(rows) + } + } + + private fun List?.toTimeLine() : List { + val acceptedProblems = mutableSetOf() + return (this ?: emptyList()).mapNotNull { + when (val result = it.result) { + is RunResult.ICPC -> { + if (!acceptedProblems.contains(it.problemId)) { + if (result.verdict.isAccepted) { + acceptedProblems.add(it.problemId) + } + ICPC(it.time, it.problemId, result.verdict.isAccepted, result.verdict.shortName) + } else { + null + } + } + + is RunResult.IOI -> { + if (result.difference > 0) { + IOI(it.time, it.problemId, result.scoreAfter) + } else { + null + } + } + + is RunResult.InProgress -> { + InProgress(it.time, it.problemId) + } + } + } + } + + +} \ No newline at end of file diff --git a/src/cds/core/api/core.api b/src/cds/core/api/core.api index 2ef8fcc12..85baf5143 100644 --- a/src/cds/core/api/core.api +++ b/src/cds/core/api/core.api @@ -85,7 +85,7 @@ public final class org/icpclive/cds/adapters/FirstToSolveAdapterKt { public static final fun addFirstToSolves (Lkotlinx/coroutines/flow/Flow;)Lkotlinx/coroutines/flow/Flow; } -public final class org/icpclive/cds/adapters/HiddenProbblemsAdapterKt { +public final class org/icpclive/cds/adapters/HiddenProblemsAdapterKt { public static final fun processHiddenProblems (Lkotlinx/coroutines/flow/Flow;)Lkotlinx/coroutines/flow/Flow; } @@ -1672,110 +1672,6 @@ public final class org/icpclive/cds/api/TeamMediaType$Companion { public final fun serializer ()Lkotlinx/serialization/KSerializer; } -public abstract class org/icpclive/cds/api/TimeLineRunInfo { - public static final field Companion Lorg/icpclive/cds/api/TimeLineRunInfo$Companion; - public synthetic fun (ILkotlinx/serialization/internal/SerializationConstructorMarker;)V - public static final synthetic fun write$Self (Lorg/icpclive/cds/api/TimeLineRunInfo;Lkotlinx/serialization/encoding/CompositeEncoder;Lkotlinx/serialization/descriptors/SerialDescriptor;)V -} - -public final class org/icpclive/cds/api/TimeLineRunInfo$Companion { - public final fun fromRunInfo (Lorg/icpclive/cds/api/RunInfo;Ljava/util/Set;)Lorg/icpclive/cds/api/TimeLineRunInfo; - public final fun serializer ()Lkotlinx/serialization/KSerializer; -} - -public final class org/icpclive/cds/api/TimeLineRunInfo$ICPC : org/icpclive/cds/api/TimeLineRunInfo { - public static final field Companion Lorg/icpclive/cds/api/TimeLineRunInfo$ICPC$Companion; - public synthetic fun (JLjava/lang/String;ZLjava/lang/String;Lkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun component1-UwyO8pc ()J - public final fun component2-Xzdl60o ()Ljava/lang/String; - public final fun component3 ()Z - public final fun component4 ()Ljava/lang/String; - public final fun copy-4joZhgo (JLjava/lang/String;ZLjava/lang/String;)Lorg/icpclive/cds/api/TimeLineRunInfo$ICPC; - public static synthetic fun copy-4joZhgo$default (Lorg/icpclive/cds/api/TimeLineRunInfo$ICPC;JLjava/lang/String;ZLjava/lang/String;ILjava/lang/Object;)Lorg/icpclive/cds/api/TimeLineRunInfo$ICPC; - public fun equals (Ljava/lang/Object;)Z - public final fun getProblemId-Xzdl60o ()Ljava/lang/String; - public final fun getShortName ()Ljava/lang/String; - public final fun getTime-UwyO8pc ()J - public fun hashCode ()I - public final fun isAccepted ()Z - public fun toString ()Ljava/lang/String; -} - -public synthetic class org/icpclive/cds/api/TimeLineRunInfo$ICPC$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Lorg/icpclive/cds/api/TimeLineRunInfo$ICPC$$serializer; - public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; - public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; - public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lorg/icpclive/cds/api/TimeLineRunInfo$ICPC; - public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; - public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V - public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lorg/icpclive/cds/api/TimeLineRunInfo$ICPC;)V - public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; -} - -public final class org/icpclive/cds/api/TimeLineRunInfo$ICPC$Companion { - public final fun serializer ()Lkotlinx/serialization/KSerializer; -} - -public final class org/icpclive/cds/api/TimeLineRunInfo$IOI : org/icpclive/cds/api/TimeLineRunInfo { - public static final field Companion Lorg/icpclive/cds/api/TimeLineRunInfo$IOI$Companion; - public synthetic fun (JLjava/lang/String;DLkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun component1-UwyO8pc ()J - public final fun component2-Xzdl60o ()Ljava/lang/String; - public final fun component3 ()D - public final fun copy-nveW3u0 (JLjava/lang/String;D)Lorg/icpclive/cds/api/TimeLineRunInfo$IOI; - public static synthetic fun copy-nveW3u0$default (Lorg/icpclive/cds/api/TimeLineRunInfo$IOI;JLjava/lang/String;DILjava/lang/Object;)Lorg/icpclive/cds/api/TimeLineRunInfo$IOI; - public fun equals (Ljava/lang/Object;)Z - public final fun getProblemId-Xzdl60o ()Ljava/lang/String; - public final fun getScore ()D - public final fun getTime-UwyO8pc ()J - public fun hashCode ()I - public fun toString ()Ljava/lang/String; -} - -public synthetic class org/icpclive/cds/api/TimeLineRunInfo$IOI$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Lorg/icpclive/cds/api/TimeLineRunInfo$IOI$$serializer; - public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; - public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; - public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lorg/icpclive/cds/api/TimeLineRunInfo$IOI; - public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; - public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V - public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lorg/icpclive/cds/api/TimeLineRunInfo$IOI;)V - public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; -} - -public final class org/icpclive/cds/api/TimeLineRunInfo$IOI$Companion { - public final fun serializer ()Lkotlinx/serialization/KSerializer; -} - -public final class org/icpclive/cds/api/TimeLineRunInfo$InProgress : org/icpclive/cds/api/TimeLineRunInfo { - public static final field Companion Lorg/icpclive/cds/api/TimeLineRunInfo$InProgress$Companion; - public synthetic fun (JLjava/lang/String;Lkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun component1-UwyO8pc ()J - public final fun component2-Xzdl60o ()Ljava/lang/String; - public final fun copy-15fPPug (JLjava/lang/String;)Lorg/icpclive/cds/api/TimeLineRunInfo$InProgress; - public static synthetic fun copy-15fPPug$default (Lorg/icpclive/cds/api/TimeLineRunInfo$InProgress;JLjava/lang/String;ILjava/lang/Object;)Lorg/icpclive/cds/api/TimeLineRunInfo$InProgress; - public fun equals (Ljava/lang/Object;)Z - public final fun getProblemId-Xzdl60o ()Ljava/lang/String; - public final fun getTime-UwyO8pc ()J - public fun hashCode ()I - public fun toString ()Ljava/lang/String; -} - -public synthetic class org/icpclive/cds/api/TimeLineRunInfo$InProgress$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Lorg/icpclive/cds/api/TimeLineRunInfo$InProgress$$serializer; - public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; - public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; - public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lorg/icpclive/cds/api/TimeLineRunInfo$InProgress; - public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; - public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V - public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lorg/icpclive/cds/api/TimeLineRunInfo$InProgress;)V - public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; -} - -public final class org/icpclive/cds/api/TimeLineRunInfo$InProgress$Companion { - public final fun serializer ()Lkotlinx/serialization/KSerializer; -} - public abstract class org/icpclive/cds/api/Verdict { public static final field Companion Lorg/icpclive/cds/api/Verdict$Companion; public synthetic fun (Ljava/lang/String;ZZLkotlin/jvm/internal/DefaultConstructorMarker;)V @@ -2399,3 +2295,10 @@ public final class org/icpclive/cds/tunning/TeamRegexOverrides$Companion { public final fun serializer ()Lkotlinx/serialization/KSerializer; } +public final class org/icpclive/cds/utils/TeamRunsStorage { + public fun ()V + public final fun applyEvent (Lorg/icpclive/cds/api/ContestState;)Ljava/util/List; + public final fun getRuns-nUE0bHc (Ljava/lang/String;)Ljava/util/List; + public final fun updateRun (Lorg/icpclive/cds/api/RunInfo;Lorg/icpclive/cds/api/RunInfo;)Ljava/util/List; +} + diff --git a/src/cds/core/src/main/kotlin/org/icpclive/cds/adapters/AutoFinalize.kt b/src/cds/core/src/main/kotlin/org/icpclive/cds/adapters/AutoFinalize.kt index 88f009e1c..7da5ad871 100644 --- a/src/cds/core/src/main/kotlin/org/icpclive/cds/adapters/AutoFinalize.kt +++ b/src/cds/core/src/main/kotlin/org/icpclive/cds/adapters/AutoFinalize.kt @@ -5,6 +5,7 @@ import org.icpclive.cds.ContestUpdate import org.icpclive.cds.InfoUpdate import org.icpclive.cds.api.* import org.icpclive.cds.util.getLogger +import org.icpclive.cds.utils.withGroupedRuns private val logger by getLogger() diff --git a/src/cds/core/src/main/kotlin/org/icpclive/cds/adapters/DifferenceAdapter.kt b/src/cds/core/src/main/kotlin/org/icpclive/cds/adapters/DifferenceAdapter.kt index 4d1428f5e..0131aeef7 100644 --- a/src/cds/core/src/main/kotlin/org/icpclive/cds/adapters/DifferenceAdapter.kt +++ b/src/cds/core/src/main/kotlin/org/icpclive/cds/adapters/DifferenceAdapter.kt @@ -4,6 +4,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map import org.icpclive.cds.ContestUpdate import org.icpclive.cds.api.* +import org.icpclive.cds.utils.withGroupedRuns private interface ScoreAccumulator { fun add(score: RunResult.IOI) diff --git a/src/cds/core/src/main/kotlin/org/icpclive/cds/adapters/FirstToSolveAdapter.kt b/src/cds/core/src/main/kotlin/org/icpclive/cds/adapters/FirstToSolveAdapter.kt index 8af555c30..0f245aa35 100644 --- a/src/cds/core/src/main/kotlin/org/icpclive/cds/adapters/FirstToSolveAdapter.kt +++ b/src/cds/core/src/main/kotlin/org/icpclive/cds/adapters/FirstToSolveAdapter.kt @@ -4,7 +4,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map import org.icpclive.cds.ContestUpdate import org.icpclive.cds.api.* -import java.rmi.NotBoundException +import org.icpclive.cds.utils.withGroupedRuns private fun RunInfo.setFTS(value: Boolean) = when (result) { is RunResult.ICPC -> copy(result = result.copy(isFirstToSolveRun = value)) diff --git a/src/cds/core/src/main/kotlin/org/icpclive/cds/adapters/HiddenProbblemsAdapter.kt b/src/cds/core/src/main/kotlin/org/icpclive/cds/adapters/HiddenProblemsAdapter.kt similarity index 94% rename from src/cds/core/src/main/kotlin/org/icpclive/cds/adapters/HiddenProbblemsAdapter.kt rename to src/cds/core/src/main/kotlin/org/icpclive/cds/adapters/HiddenProblemsAdapter.kt index 8a963d625..fe7cc78fa 100644 --- a/src/cds/core/src/main/kotlin/org/icpclive/cds/adapters/HiddenProbblemsAdapter.kt +++ b/src/cds/core/src/main/kotlin/org/icpclive/cds/adapters/HiddenProblemsAdapter.kt @@ -5,6 +5,7 @@ import kotlinx.coroutines.flow.map import org.icpclive.cds.api.ContestInfo import org.icpclive.cds.ContestUpdate import org.icpclive.cds.api.ProblemId +import org.icpclive.cds.utils.withGroupedRuns public fun Flow.processHiddenProblems(): Flow = withGroupedRuns( diff --git a/src/cds/core/src/main/kotlin/org/icpclive/cds/adapters/HiddenTeamsAdapter.kt b/src/cds/core/src/main/kotlin/org/icpclive/cds/adapters/HiddenTeamsAdapter.kt index a63acd05b..f978a46ba 100644 --- a/src/cds/core/src/main/kotlin/org/icpclive/cds/adapters/HiddenTeamsAdapter.kt +++ b/src/cds/core/src/main/kotlin/org/icpclive/cds/adapters/HiddenTeamsAdapter.kt @@ -5,6 +5,7 @@ import kotlinx.coroutines.flow.map import org.icpclive.cds.ContestUpdate import org.icpclive.cds.InfoUpdate import org.icpclive.cds.api.* +import org.icpclive.cds.utils.withGroupedRuns private fun TeamInfo.updateHidden(isHidden: Boolean, isOutOfContest: Boolean) = if (isHidden != this.isHidden || isOutOfContest != this.isOutOfContest) { diff --git a/src/cds/core/src/main/kotlin/org/icpclive/cds/adapters/SelectProblemColorAdapter.kt b/src/cds/core/src/main/kotlin/org/icpclive/cds/adapters/SelectProblemColorAdapter.kt index 41a71a49e..d5855b73e 100644 --- a/src/cds/core/src/main/kotlin/org/icpclive/cds/adapters/SelectProblemColorAdapter.kt +++ b/src/cds/core/src/main/kotlin/org/icpclive/cds/adapters/SelectProblemColorAdapter.kt @@ -4,6 +4,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.transform import org.icpclive.cds.* import org.icpclive.cds.api.* +import org.icpclive.cds.utils.withGroupedRuns private fun RunInfo.shouldDiscloseColor() = (result as? RunResult.ICPC)?.verdict?.isAccepted == true && !isHidden diff --git a/src/cds/core/src/main/kotlin/org/icpclive/cds/api/TimeLineRunInfo.kt b/src/cds/core/src/main/kotlin/org/icpclive/cds/api/TimeLineRunInfo.kt deleted file mode 100644 index d3e73f4eb..000000000 --- a/src/cds/core/src/main/kotlin/org/icpclive/cds/api/TimeLineRunInfo.kt +++ /dev/null @@ -1,56 +0,0 @@ -package org.icpclive.cds.api - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -import org.icpclive.cds.util.serializers.DurationInMillisecondsSerializer -import kotlin.time.Duration - -@Serializable -public sealed class TimeLineRunInfo { - @Serializable - @SerialName("ICPC") - public data class ICPC( - @Serializable(with = DurationInMillisecondsSerializer::class) val time: Duration, - val problemId: ProblemId, - val isAccepted: Boolean, - val shortName: String) : TimeLineRunInfo() - - @Serializable - @SerialName("IOI") - public data class IOI(@Serializable(with = DurationInMillisecondsSerializer::class) val time: Duration, val problemId: ProblemId, val score: Double) : TimeLineRunInfo() - - @Serializable - @SerialName("IN_PROGRESS") - public data class InProgress(@Serializable(with = DurationInMillisecondsSerializer::class) val time: Duration, val problemId: ProblemId) : TimeLineRunInfo() - - public companion object { - public fun fromRunInfo(info: RunInfo, acceptedProblems: MutableSet): TimeLineRunInfo? { - return when (info.result) { - is RunResult.ICPC -> { - val icpcResult = info.result - if (!acceptedProblems.contains(info.problemId)) { - if (icpcResult.verdict.isAccepted) { - acceptedProblems.add(info.problemId) - } - ICPC(info.time, info.problemId, icpcResult.verdict.isAccepted, icpcResult.verdict.shortName) - } else { - null - } - } - - is RunResult.IOI -> { - val ioiResult = info.result - if (ioiResult.difference > 0) { - IOI(info.time, info.problemId, ioiResult.scoreAfter) - } else { - null - } - } - - else -> { - InProgress(info.time, info.problemId) - } - } - } - } -} diff --git a/src/cds/core/src/main/kotlin/org/icpclive/cds/scoreboard/AbstractScoreboardCalculator.kt b/src/cds/core/src/main/kotlin/org/icpclive/cds/scoreboard/AbstractScoreboardCalculator.kt index 13219539f..da1587067 100644 --- a/src/cds/core/src/main/kotlin/org/icpclive/cds/scoreboard/AbstractScoreboardCalculator.kt +++ b/src/cds/core/src/main/kotlin/org/icpclive/cds/scoreboard/AbstractScoreboardCalculator.kt @@ -3,9 +3,10 @@ package org.icpclive.cds.scoreboard import kotlinx.collections.immutable.* import kotlinx.coroutines.flow.* import org.icpclive.cds.* -import org.icpclive.cds.adapters.* +import org.icpclive.cds.utils.TeamRunsStorage +import org.icpclive.cds.adapters.contestState import org.icpclive.cds.api.* -import org.icpclive.cds.util.* +import org.icpclive.cds.util.getLogger import kotlin.time.Duration public class Ranking internal constructor( @@ -203,13 +204,11 @@ public class ContestStateWithScoreboard internal constructor( public fun scoreboardRowAfter(teamId: TeamId): ScoreboardRow = scoreboardRowsAfter[teamId]!! } -private fun RunInfo.isTested() = !isHidden && result !is RunResult.InProgress - public fun Flow.calculateScoreboard(optimismLevel: OptimismLevel): Flow = flow { var rows = persistentMapOf() var lastRanking = Ranking(emptyList(), emptyList(), emptyList()) var lastSubmissionTime: Duration = Duration.ZERO - var runsByTeamId = persistentMapOf>() + val runsByTeamId = TeamRunsStorage() fun applyEvent(state: ContestState) : List { val info = state.infoAfterEvent ?: return emptyList() val calculator = getScoreboardCalculator(info, optimismLevel) @@ -217,25 +216,12 @@ public fun Flow.calculateScoreboard(optimismLevel: OptimismLevel) is AnalyticsUpdate -> emptyList() is InfoUpdate -> info.teams.keys.toList() is RunUpdate -> { - val oldRun = state.runsBeforeEvent[event.newInfo.id] - val newRun = event.newInfo - if (oldRun?.teamId != newRun.teamId) { - if (oldRun != null) { - runsByTeamId = runsByTeamId.removeRun(oldRun.teamId, oldRun) - } - runsByTeamId = runsByTeamId.addAndResort(newRun.teamId, newRun) - } else { - runsByTeamId = runsByTeamId.updateAndResort(newRun.teamId, newRun) - } - lastSubmissionTime = maxOf(lastSubmissionTime, newRun.time) - - listOfNotNull(oldRun?.teamId, newRun.teamId).distinct().takeIf { - oldRun == null || oldRun.isTested() || newRun.isTested() - } ?: emptyList() + lastSubmissionTime = maxOf(lastSubmissionTime, event.newInfo.time) + runsByTeamId.applyEvent(state) } } val teamsReallyAffected = teamsAffected.filter { - val newRow = calculator.getScoreboardRow(info, runsByTeamId[it] ?: emptyList()) + val newRow = calculator.getScoreboardRow(info, runsByTeamId.getRuns(it)) val oldRow = rows[it] rows = rows.put(it, newRow) newRow != oldRow diff --git a/src/cds/core/src/main/kotlin/org/icpclive/cds/adapters/Utils.kt b/src/cds/core/src/main/kotlin/org/icpclive/cds/utils/ContestStateWithGroupedRuns.kt similarity index 74% rename from src/cds/core/src/main/kotlin/org/icpclive/cds/adapters/Utils.kt rename to src/cds/core/src/main/kotlin/org/icpclive/cds/utils/ContestStateWithGroupedRuns.kt index 829f3f7c5..ea8f374f2 100644 --- a/src/cds/core/src/main/kotlin/org/icpclive/cds/adapters/Utils.kt +++ b/src/cds/core/src/main/kotlin/org/icpclive/cds/utils/ContestStateWithGroupedRuns.kt @@ -1,4 +1,4 @@ -package org.icpclive.cds.adapters +package org.icpclive.cds.utils import kotlinx.collections.immutable.* import kotlinx.coroutines.flow.Flow @@ -15,41 +15,6 @@ internal open class ContestStateWithGroupedRuns( val infoAfterEvent: ContestInfo? get() = if (event is InfoUpdate) event.newInfo else infoBeforeEvent } -private fun PersistentList.resort(index_: Int) = builder().apply { - var index = index_ - val comparator = compareBy(RunInfo::time, { it.id.value }) - while (index > 0 && comparator.compare(get(index - 1), get(index)) > 0) { - val t = get(index) - set(index, get(index - 1)) - set(index - 1, t) - index-- - } - while (index + 1 < size && comparator.compare(get(index), get(index + 1)) > 0) { - val t = get(index) - set(index, get(index + 1)) - set(index + 1, t) - index++ - } -}.build() - -private fun PersistentList.addAndResort(info: RunInfo) = add(info).resort(size) -private fun PersistentList.setAndResort(index: Int, info: RunInfo) = set(index, info).resort(index) - -internal inline fun PersistentMap.update(k: K, block: (V?) -> V) = put(k, block(get(k))) - -internal fun PersistentMap>.addAndResort(k: K, info: RunInfo) = update(k) { - (it ?: persistentListOf()).addAndResort(info) -} - -internal fun PersistentMap>.updateAndResort(k: K, info: RunInfo) = update(k) { - val index = it!!.indexOfFirst { run -> run.id == info.id } - it.setAndResort(index, info) -} - -internal fun PersistentMap>.removeRun(k: K, info: RunInfo) = update(k) { - val index = it!!.indexOfFirst { run -> run.id == info.id } - it.removeAt(index) -} internal fun Flow.withGroupedRuns( selector: (RunInfo) -> K, diff --git a/src/cds/core/src/main/kotlin/org/icpclive/cds/utils/TeamRunsStorage.kt b/src/cds/core/src/main/kotlin/org/icpclive/cds/utils/TeamRunsStorage.kt new file mode 100644 index 000000000..cc06aaaef --- /dev/null +++ b/src/cds/core/src/main/kotlin/org/icpclive/cds/utils/TeamRunsStorage.kt @@ -0,0 +1,36 @@ +package org.icpclive.cds.utils + +import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.persistentMapOf +import org.icpclive.cds.RunUpdate +import org.icpclive.cds.api.* + +public class TeamRunsStorage { + private fun RunInfo.isTested() = !isHidden && result !is RunResult.InProgress + private var runsByTeamId = persistentMapOf>() + + public fun applyEvent(state: ContestState) : List { + if (state.lastEvent !is RunUpdate) return emptyList() + val oldRun = state.runsBeforeEvent[state.lastEvent.newInfo.id] + val newRun = state.lastEvent.newInfo + return updateRun(oldRun, newRun) + } + + public fun updateRun(oldRun: RunInfo?, newRun: RunInfo) : List { + if (oldRun?.teamId != newRun.teamId) { + if (oldRun != null) { + runsByTeamId = runsByTeamId.removeRun(oldRun.teamId, oldRun) + } + runsByTeamId = runsByTeamId.addAndResort(newRun.teamId, newRun) + } else { + runsByTeamId = runsByTeamId.updateAndResort(newRun.teamId, newRun) + } + return listOfNotNull(oldRun?.teamId, newRun.teamId).distinct().takeIf { + oldRun == null || oldRun.isTested() || newRun.isTested() + } ?: emptyList() + } + + public fun getRuns(key: TeamId) : List { + return runsByTeamId[key] ?: emptyList() + } +} \ No newline at end of file diff --git a/src/cds/core/src/main/kotlin/org/icpclive/cds/utils/Utils.kt b/src/cds/core/src/main/kotlin/org/icpclive/cds/utils/Utils.kt new file mode 100644 index 000000000..9f8902cda --- /dev/null +++ b/src/cds/core/src/main/kotlin/org/icpclive/cds/utils/Utils.kt @@ -0,0 +1,41 @@ +package org.icpclive.cds.utils + +import kotlinx.collections.immutable.* +import org.icpclive.cds.api.* + +private fun PersistentList.resort(index_: Int) = builder().apply { + var index = index_ + val comparator = compareBy(RunInfo::time, { it.id.value }) + while (index > 0 && comparator.compare(get(index - 1), get(index)) > 0) { + val t = get(index) + set(index, get(index - 1)) + set(index - 1, t) + index-- + } + while (index + 1 < size && comparator.compare(get(index), get(index + 1)) > 0) { + val t = get(index) + set(index, get(index + 1)) + set(index + 1, t) + index++ + } +}.build() + +private fun PersistentList.addAndResort(info: RunInfo) = add(info).resort(size) +private fun PersistentList.setAndResort(index: Int, info: RunInfo) = set(index, info).resort(index) + +internal inline fun PersistentMap.update(k: K, block: (V?) -> V) = put(k, block(get(k))) + +internal fun PersistentMap>.addAndResort(k: K, info: RunInfo) = update(k) { + (it ?: persistentListOf()).addAndResort(info) +} + +internal fun PersistentMap>.updateAndResort(k: K, info: RunInfo) = update(k) { + val index = it!!.indexOfFirst { run -> run.id == info.id } + it.setAndResort(index, info) +} + +internal fun PersistentMap>.removeRun(k: K, info: RunInfo) = update(k) { + val index = it!!.indexOfFirst { run -> run.id == info.id } + it.removeAt(index) +} +