Skip to content

Commit 4af2b5d

Browse files
committed
[JEWEL-1137] Added FPS counter to the Jewel sample app
- Added a simple way to track the app frame rate to help identify any possible bottlenecks
1 parent b9ea766 commit 4af2b5d

File tree

4 files changed

+149
-7
lines changed

4 files changed

+149
-7
lines changed
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
2+
package org.jetbrains.jewel.samples.standalone.components
3+
4+
import androidx.compose.foundation.clickable
5+
import androidx.compose.foundation.interaction.MutableInteractionSource
6+
import androidx.compose.foundation.layout.Arrangement
7+
import androidx.compose.foundation.layout.Row
8+
import androidx.compose.runtime.Composable
9+
import androidx.compose.runtime.LaunchedEffect
10+
import androidx.compose.runtime.derivedStateOf
11+
import androidx.compose.runtime.getValue
12+
import androidx.compose.runtime.mutableIntStateOf
13+
import androidx.compose.runtime.mutableStateOf
14+
import androidx.compose.runtime.remember
15+
import androidx.compose.runtime.setValue
16+
import androidx.compose.runtime.withFrameMillis
17+
import androidx.compose.ui.Modifier
18+
import androidx.compose.ui.graphics.Color
19+
import androidx.compose.ui.unit.dp
20+
import kotlin.math.roundToInt
21+
import kotlinx.coroutines.CoroutineDispatcher
22+
import kotlinx.coroutines.Dispatchers
23+
import kotlinx.coroutines.delay
24+
import kotlinx.coroutines.launch
25+
import org.jetbrains.jewel.ui.component.Text
26+
27+
@Composable
28+
internal fun FpsCounter(modifier: Modifier = Modifier, dispatcher: CoroutineDispatcher = Dispatchers.Default) {
29+
var displayedFPS by remember { mutableIntStateOf(0) }
30+
var textContent by remember { mutableStateOf("FPS:") }
31+
var minMaxContent by remember { mutableStateOf("") }
32+
var fpsCountMethod by remember { mutableStateOf(FPSCountMethod.RealTime) }
33+
var minFps by remember { mutableIntStateOf(240) }
34+
var maxFps by remember { mutableIntStateOf(0) }
35+
36+
val textColor by remember {
37+
derivedStateOf {
38+
when (fpsCountMethod) {
39+
FPSCountMethod.RealTime -> {
40+
textContent = "FPS(Realtime):$displayedFPS"
41+
}
42+
43+
FPSCountMethod.FixedInterval -> {
44+
textContent = "FPS(last ${FPS_UPDATE_DELAY}ms):$displayedFPS"
45+
}
46+
47+
FPSCountMethod.FixedFrameCount -> {
48+
textContent = "FPS(last ${FRAME_COUNT} frames):$displayedFPS"
49+
}
50+
}
51+
minMaxContent = "min:$minFps, max:$maxFps"
52+
displayedFPS.fpsCountColor
53+
}
54+
}
55+
56+
Row(modifier = modifier, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
57+
Text(
58+
text = textContent,
59+
modifier =
60+
Modifier.clickable(interactionSource = remember { MutableInteractionSource() }, indication = null) {
61+
fpsCountMethod =
62+
when (fpsCountMethod) {
63+
FPSCountMethod.FixedInterval -> FPSCountMethod.FixedFrameCount
64+
FPSCountMethod.FixedFrameCount -> FPSCountMethod.RealTime
65+
FPSCountMethod.RealTime -> FPSCountMethod.FixedInterval
66+
}
67+
minFps = 240
68+
maxFps = 0
69+
},
70+
color = textColor,
71+
)
72+
73+
Text(
74+
text = minMaxContent,
75+
modifier =
76+
Modifier.clickable(interactionSource = remember { MutableInteractionSource() }, indication = null) {
77+
minFps = 240
78+
maxFps = 0
79+
},
80+
)
81+
}
82+
83+
LaunchedEffect(Unit) {
84+
val fpsArray = FloatArray(FRAME_COUNT) { 0f }
85+
var fpsCount = 0
86+
var writeIndex = 0
87+
var lastWriteIndex = 0
88+
var lastUpdTime = System.currentTimeMillis()
89+
90+
launch(dispatcher) {
91+
while (true) {
92+
delay(FPS_UPDATE_DELAY)
93+
94+
displayedFPS =
95+
when (fpsCountMethod) {
96+
FPSCountMethod.FixedInterval -> fpsCount * 1000 / FPS_UPDATE_DELAY.toInt()
97+
FPSCountMethod.FixedFrameCount -> fpsArray.average().roundToInt()
98+
FPSCountMethod.RealTime -> fpsArray[lastWriteIndex].roundToInt()
99+
}
100+
if (displayedFPS > 0) {
101+
minFps = minOf(minFps, displayedFPS)
102+
}
103+
maxFps = maxOf(maxFps, displayedFPS)
104+
fpsCount = 0
105+
}
106+
}
107+
108+
while (true) {
109+
withFrameMillis { frameTimeMillis ->
110+
fpsCount++
111+
112+
fpsArray[writeIndex] = 1000f / (frameTimeMillis - lastUpdTime)
113+
lastUpdTime = frameTimeMillis
114+
115+
lastWriteIndex = writeIndex
116+
writeIndex = (writeIndex + 1) % fpsArray.size
117+
}
118+
}
119+
}
120+
}
121+
122+
private const val FPS_UPDATE_DELAY = 250L
123+
private const val FRAME_COUNT = 20
124+
125+
private val Int.fpsCountColor: Color
126+
get() = when {
127+
this > 55 -> Color.Green
128+
this > 45 -> Color.Yellow
129+
else -> Color.Red
130+
}
131+
132+
internal enum class FPSCountMethod {
133+
FixedInterval,
134+
FixedFrameCount,
135+
RealTime,
136+
}

platform/jewel/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/view/TitleBarView.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import java.net.URI
1515
import org.jetbrains.jewel.samples.showcase.ShowcaseIcons
1616
import org.jetbrains.jewel.samples.showcase.views.forCurrentOs
1717
import org.jetbrains.jewel.samples.standalone.IntUiThemes
18+
import org.jetbrains.jewel.samples.standalone.components.FpsCounter
1819
import org.jetbrains.jewel.samples.standalone.viewmodel.MainViewModel
1920
import org.jetbrains.jewel.ui.component.Dropdown
2021
import org.jetbrains.jewel.ui.component.Icon
@@ -69,6 +70,10 @@ internal fun DecoratedWindowScope.TitleBarView() {
6970
Text(title)
7071

7172
Row(Modifier.align(Alignment.End)) {
73+
if (MainViewModel.showFPSCount) {
74+
FpsCounter(modifier = Modifier.align(Alignment.CenterVertically))
75+
}
76+
7277
Tooltip({ Text("Open Jewel Github repository") }) {
7378
val jewelGithubLink = "https://github.com/JetBrains/intellij-community/tree/master/platform/jewel"
7479
IconButton(

platform/jewel/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/view/WelcomeView.kt

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ import org.jetbrains.jewel.ui.component.CheckboxRow
2424
import org.jetbrains.jewel.ui.component.Icon
2525
import org.jetbrains.jewel.ui.component.RadioButtonChip
2626
import org.jetbrains.jewel.ui.component.Text
27-
import org.jetbrains.jewel.ui.component.styling.LocalCheckboxStyle
2827
import org.jetbrains.jewel.ui.icon.IconKey
2928
import org.jetbrains.jewel.ui.painter.hints.Selected
3029
import org.jetbrains.jewel.ui.typography
@@ -71,9 +70,6 @@ internal fun WelcomeView() {
7170
text = "Swing compatibility",
7271
checked = MainViewModel.swingCompat,
7372
onCheckedChange = { MainViewModel.swingCompat = it },
74-
colors = LocalCheckboxStyle.current.colors,
75-
metrics = LocalCheckboxStyle.current.metrics,
76-
icons = LocalCheckboxStyle.current.icons,
7773
)
7874

7975
CheckboxRow(
@@ -83,9 +79,12 @@ internal fun WelcomeView() {
8379
MainViewModel.useCustomPopupRenderer = it
8480
JewelFlags.useCustomPopupRenderer = it
8581
},
86-
colors = LocalCheckboxStyle.current.colors,
87-
metrics = LocalCheckboxStyle.current.metrics,
88-
icons = LocalCheckboxStyle.current.icons,
82+
)
83+
84+
CheckboxRow(
85+
text = "Show FPS Count",
86+
checked = MainViewModel.showFPSCount,
87+
onCheckedChange = { MainViewModel.showFPSCount = it },
8988
)
9089
}
9190
}

platform/jewel/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/viewmodel/MainViewModel.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ public object MainViewModel {
4545

4646
public var theme: IntUiThemes by mutableStateOf(IntUiThemes.Light)
4747

48+
public var showFPSCount: Boolean by mutableStateOf(false)
49+
4850
public var swingCompat: Boolean by mutableStateOf(false)
4951

5052
public var useCustomPopupRenderer: Boolean by mutableStateOf(JewelFlags.useCustomPopupRenderer)

0 commit comments

Comments
 (0)