Skip to content

Commit

Permalink
Compute statistics on time for each widget.
Browse files Browse the repository at this point in the history
  • Loading branch information
kunyavskiy authored and kbats183 committed Sep 8, 2024
1 parent 3a6bd13 commit 59018c9
Show file tree
Hide file tree
Showing 11 changed files with 203 additions and 21 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package org.icpclive.api

import kotlinx.serialization.*
import org.icpclive.cds.api.TeamId
import org.icpclive.cds.util.serializers.DurationInSecondsSerializer
import kotlin.time.*

@Serializable
data class WidgetUsageStatistics(
val entries: MutableMap<String, WidgetUsageStatisticsEntry>
)

@Serializable
sealed class WidgetUsageStatisticsEntry {
@Serializable
@SerialName("simple")
data class Simple(
@Transient val shownSince: TimeSource.Monotonic.ValueTimeMark? = null,
@Transient val shownCount: Int = 0,
@Serializable(with = DurationInSecondsSerializer::class)
@SerialName("totalShownTimeSeconds")
val totalShownTime: Duration
) : WidgetUsageStatisticsEntry()
@Serializable
@SerialName("per_team")
data class PerTeam(val byTeam: Map<TeamId, WidgetUsageStatisticsEntry>): WidgetUsageStatisticsEntry()
}
35 changes: 24 additions & 11 deletions src/backend-api/src/main/kotlin/org/icpclive/api/Widgets.kt
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,16 @@ fun getLocationOrDefault(widgetPrefix: String, defaultLocationRectangle: Locatio
@Serializable
sealed class Widget(
@SerialName("widgetId") override val id: String,
val location: LocationRectangle
val location: LocationRectangle,
val statisticsId: String,
) : TypeWithId

@Serializable
@SerialName("AdvertisementWidget")
class AdvertisementWidget(val advertisement: AdvertisementSettings) : Widget(
generateId(WIDGET_ID_PREFIX),
getLocationOrDefault(WIDGET_ID_PREFIX, location)
getLocationOrDefault(WIDGET_ID_PREFIX, location),
WIDGET_ID_PREFIX,
) {
companion object {
const val WIDGET_ID_PREFIX = "advertisement"
Expand All @@ -44,7 +46,8 @@ class AdvertisementWidget(val advertisement: AdvertisementSettings) : Widget(
@SerialName("PictureWidget")
class PictureWidget(val picture: PictureSettings) : Widget(
generateId(WIDGET_ID_PREFIX),
getLocationOrDefault(WIDGET_ID_PREFIX, location)
getLocationOrDefault(WIDGET_ID_PREFIX, location),
WIDGET_ID_PREFIX
) {
companion object {
const val WIDGET_ID_PREFIX = "picture"
Expand All @@ -56,7 +59,8 @@ class PictureWidget(val picture: PictureSettings) : Widget(
@SerialName("SvgWidget")
class SvgWidget(val content: String) : Widget(
generateId(WIDGET_ID_PREFIX),
getLocationOrDefault(WIDGET_ID_PREFIX, location)
getLocationOrDefault(WIDGET_ID_PREFIX, location),
WIDGET_ID_PREFIX
) {
companion object {
const val WIDGET_ID_PREFIX = "svg"
Expand All @@ -68,7 +72,8 @@ class SvgWidget(val content: String) : Widget(
@SerialName("QueueWidget")
class QueueWidget(val settings: QueueSettings) : Widget(
WIDGET_ID,
getLocationOrDefault(WIDGET_ID, location)
getLocationOrDefault(WIDGET_ID, location),
WIDGET_ID,
) {
companion object {
const val WIDGET_ID = "queue"
Expand All @@ -80,7 +85,8 @@ class QueueWidget(val settings: QueueSettings) : Widget(
@SerialName("ScoreboardWidget")
class ScoreboardWidget(val settings: ScoreboardSettings) : Widget(
WIDGET_ID,
getLocationOrDefault(WIDGET_ID, location)
getLocationOrDefault(WIDGET_ID, location),
WIDGET_ID,
) {
companion object {
const val WIDGET_ID = "scoreboard"
Expand All @@ -92,7 +98,8 @@ class ScoreboardWidget(val settings: ScoreboardSettings) : Widget(
@SerialName("StatisticsWidget")
class StatisticsWidget(val settings: StatisticsSettings) : Widget(
WIDGET_ID,
getLocationOrDefault(WIDGET_ID, location)
getLocationOrDefault(WIDGET_ID, location),
WIDGET_ID
) {
companion object {
const val WIDGET_ID = "statistics"
Expand All @@ -104,7 +111,8 @@ class StatisticsWidget(val settings: StatisticsSettings) : Widget(
@SerialName("TickerWidget")
class TickerWidget(val settings: TickerSettings) : Widget(
WIDGET_ID,
getLocationOrDefault(WIDGET_ID, location)
getLocationOrDefault(WIDGET_ID, location),
WIDGET_ID
) {
companion object {
const val WIDGET_ID = "ticker"
Expand All @@ -117,16 +125,19 @@ enum class TeamViewPosition {
SINGLE_TOP_RIGHT, PVP_TOP, PVP_BOTTOM, TOP_LEFT, TOP_RIGHT, BOTTOM_LEFT, BOTTOM_RIGHT
}


@Serializable
@SerialName("TeamViewWidget")
class TeamViewWidget(
val settings: OverlayTeamViewSettings
) : Widget(
getWidgetId(settings.position),
getLocationOrDefault(getWidgetId(settings.position), getLocation(settings.position))
getLocationOrDefault(getWidgetId(settings.position), getLocation(settings.position)),
WIDGET_ID_PREFIX
) {
companion object {
fun getWidgetId(position: TeamViewPosition) = "teamview." + position.name
private const val WIDGET_ID_PREFIX = "teamview"
fun getWidgetId(position: TeamViewPosition) = "$WIDGET_ID_PREFIX.${position.name}"
fun getLocation(position: TeamViewPosition) = when (position) {
TeamViewPosition.SINGLE_TOP_RIGHT -> LocationRectangle(16, 16, 1488, 984)
TeamViewPosition.PVP_TOP -> LocationRectangle(16, 16, 1488, 984 / 2 + 16)
Expand All @@ -143,7 +154,8 @@ class TeamViewWidget(
@SerialName("FullScreenClockWidget")
class FullScreenClockWidget(val settings: FullScreenClockSettings) : Widget(
WIDGET_ID,
getLocationOrDefault(WIDGET_ID, location)
getLocationOrDefault(WIDGET_ID, location),
WIDGET_ID
) {
companion object {
const val WIDGET_ID = "fullScreenClock"
Expand All @@ -156,6 +168,7 @@ class FullScreenClockWidget(val settings: FullScreenClockSettings) : Widget(
class TeamLocatorWidget(val settings: TeamLocatorSettings) : Widget(
WIDGET_ID,
getLocationOrDefault(WIDGET_ID, location),
WIDGET_ID,
) {
companion object {
const val WIDGET_ID = "teamLocator"
Expand Down
3 changes: 3 additions & 0 deletions src/backend/src/main/kotlin/org/icpclive/admin/Routing.kt
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,9 @@ fun Route.configureAdminApiRouting() {
}
}
}
get("/usage_stats") {
call.respond(Controllers.getWidgetStats())
}
}
route("/social") {
setupSocial()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import org.icpclive.api.*
import org.icpclive.cds.api.ContestInfo
import org.icpclive.data.*

class LocatorWidgetController(manager: Manager<TeamLocatorWidget>) :
class LocatorWidgetController(manager: Manager<in TeamLocatorWidget>) :
SingleWidgetController<ExternalTeamLocatorSettings, TeamLocatorWidget>(ExternalTeamLocatorSettings(), manager) {
override suspend fun onDelete(id: Int) {}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import org.icpclive.data.Manager

abstract class SingleWidgetController<SettingsType : ObjectSettings, DataType : TypeWithId>(
private var settings: SettingsType,
private val manager: Manager<DataType>,
private val manager: Manager<in DataType>,
val id: Int? = null,
) {
private val mutex = Mutex()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
package org.icpclive.controllers

import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.first
import org.icpclive.api.*
import org.icpclive.cds.api.MediaType
import org.icpclive.cds.api.TeamMediaType
import org.icpclive.data.*
import kotlin.time.Duration.Companion.seconds

class TeamViewController(manager: Manager<TeamViewWidget>, val position: TeamViewPosition) :
class TeamViewController(manager: Manager<in TeamViewWidget>, val position: TeamViewPosition) :
SingleWidgetController<ExternalTeamViewSettings, TeamViewWidget>(ExternalTeamViewSettings(), manager) {
override suspend fun onDelete(id: Int) {}

Expand Down
2 changes: 2 additions & 0 deletions src/backend/src/main/kotlin/org/icpclive/data/Controllers.kt
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,6 @@ object Controllers {
}
val tickerMessage = PresetsController(presetsPath("ticker"), TickerManager, TickerMessageSettings::toMessage)
val userController = Config.createUsersController()

suspend fun getWidgetStats() = WidgetManager.getUsageStatistics()
}
24 changes: 20 additions & 4 deletions src/backend/src/main/kotlin/org/icpclive/data/Manager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import org.icpclive.api.TypeWithId

abstract class Manager<in T> {
abstract class Manager<T> {
abstract suspend fun add(item: T)
abstract suspend fun remove(itemId: String)
}

abstract class ManagerWithEvents<in T : TypeWithId, E> : Manager<T>() {
abstract class ManagerWithEvents<T : TypeWithId, E> : Manager<T>() {
private val mutex = Mutex()
private var timer = 0L
private val items = mutableListOf<T>()
Expand All @@ -24,15 +24,31 @@ abstract class ManagerWithEvents<in T : TypeWithId, E> : Manager<T>() {
protected abstract fun createAddEvent(item: T): E
protected abstract fun createRemoveEvent(id: String): E
protected abstract fun createSnapshotEvent(items: List<T>): E
protected open fun onItemAdd(item: T) {}
protected open fun onItemRemove(item: T) {}

protected suspend fun traverse(block: (T) -> Unit) = mutex.withLock {
items.forEach(block)
}

private fun removeById(id: String) : Boolean {
val myItems = items.filter { it.id == id }
for (item in myItems) {
onItemRemove(item)
}
items.removeAll(myItems)
return myItems.isNotEmpty()
}

override suspend fun add(item: T) = mutex.withLock {
items.removeIf { it.id == item.id } // We don't need the remove event, as create considered as the set on frontend.
removeById(item.id) // We don't need the remove event, as create considered as the set on frontend.
items.add(item)
onItemAdd(item)
sendEvent(createAddEvent(item))
}

override suspend fun remove(itemId: String) = mutex.withLock {
if (items.removeIf { it.id == itemId }) {
if (removeById(itemId)) {
sendEvent(createRemoveEvent(itemId))
}
}
Expand Down
87 changes: 87 additions & 0 deletions src/backend/src/main/kotlin/org/icpclive/data/WidgetManager.kt
Original file line number Diff line number Diff line change
@@ -1,13 +1,100 @@
package org.icpclive.data

import org.icpclive.api.*
import org.icpclive.cds.util.getLogger
import org.icpclive.util.completeOrThrow
import kotlin.time.Duration
import kotlin.time.TimeSource

private val logger by getLogger()

private fun WidgetUsageStatisticsEntry.Simple?.updateStatsRemove(item: Widget) =
when {
this == null -> {
logger.warning { "Suspicious statistics for widget ${item.statisticsId}: removed, but wasn't added" }
WidgetUsageStatisticsEntry.Simple(
shownSince = null,
shownCount = 0,
totalShownTime = Duration.ZERO
)
}
else -> {
if (shownCount == 0) {
logger.warning { "Suspicious statistics for widget ${item.statisticsId}: removed, but shown count was zero" }
}
WidgetUsageStatisticsEntry.Simple(
shownSince = if (this.shownCount == 1) null else shownSince,
shownCount = shownCount - 1,
totalShownTime = totalShownTime + if (this.shownCount == 1) shownSince!!.elapsedNow() else Duration.ZERO,
)
}
}

private fun WidgetUsageStatisticsEntry.Simple?.updateStatsAdd(item: Widget) : WidgetUsageStatisticsEntry.Simple =
WidgetUsageStatisticsEntry.Simple(
shownSince = this?.shownSince ?: TimeSource.Monotonic.markNow(),
shownCount = (this?.shownCount ?: 0) + 1,
totalShownTime = (this?.totalShownTime ?: Duration.ZERO)
)


private fun WidgetUsageStatisticsEntry?.updateStatsRemove(item: Widget) : WidgetUsageStatisticsEntry = when (item) {
is TeamViewWidget -> {
require(this is WidgetUsageStatisticsEntry.PerTeam?)
val res = this ?: WidgetUsageStatisticsEntry.PerTeam(mutableMapOf())
val newEntry = (res.byTeam[item.settings.teamId] as WidgetUsageStatisticsEntry.Simple?).updateStatsRemove(item)
WidgetUsageStatisticsEntry.PerTeam(
res.byTeam + (item.settings.teamId to newEntry)
)
}
else -> {
require(this is WidgetUsageStatisticsEntry.Simple?)
updateStatsRemove(item)
}
}

private fun WidgetUsageStatisticsEntry?.updateStatsAdd(item: Widget): WidgetUsageStatisticsEntry = when (item) {
is TeamViewWidget -> {
require(this is WidgetUsageStatisticsEntry.PerTeam?)
val res = this ?: WidgetUsageStatisticsEntry.PerTeam(mutableMapOf())
val newEntry = (res.byTeam[item.settings.teamId] as WidgetUsageStatisticsEntry.Simple?).updateStatsAdd(item)
WidgetUsageStatisticsEntry.PerTeam(
res.byTeam + (item.settings.teamId to newEntry)
)
}
else -> {
require(this is WidgetUsageStatisticsEntry.Simple?)
updateStatsAdd(item)
}
}


class WidgetManager : ManagerWithEvents<Widget, MainScreenEvent>() {
private val statistics = WidgetUsageStatistics(mutableMapOf())

override fun createAddEvent(item: Widget) = ShowWidgetEvent(item)
override fun createRemoveEvent(id: String) = HideWidgetEvent(id)
override fun createSnapshotEvent(items: List<Widget>) = MainScreenSnapshotEvent(items)

override fun onItemRemove(item: Widget) {
statistics.entries[item.statisticsId] = statistics.entries[item.statisticsId].updateStatsRemove(item)
super.onItemRemove(item)
}

override fun onItemAdd(item: Widget) {
statistics.entries[item.statisticsId] = statistics.entries[item.statisticsId].updateStatsAdd(item)
super.onItemRemove(item)
}

suspend fun getUsageStatistics() : WidgetUsageStatistics {
val statisticsCopy = WidgetUsageStatistics(statistics.entries.toMutableMap())
traverse {
statisticsCopy.entries[it.statisticsId] = statisticsCopy.entries[it.statisticsId].updateStatsRemove(it)
}
return statisticsCopy
}


init {
DataBus.mainScreenFlow.completeOrThrow(flow)
}
Expand Down
Loading

0 comments on commit 59018c9

Please sign in to comment.