Skip to content

Commit caed965

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 caed965

File tree

4 files changed

+148
-7
lines changed

4 files changed

+148
-7
lines changed
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
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 java.util.concurrent.atomic.AtomicInteger
21+
import java.util.concurrent.atomic.AtomicLong
22+
import kotlin.math.roundToInt
23+
import kotlinx.coroutines.CoroutineDispatcher
24+
import kotlinx.coroutines.Dispatchers
25+
import kotlinx.coroutines.delay
26+
import kotlinx.coroutines.launch
27+
import org.jetbrains.jewel.ui.component.Text
28+
29+
@Composable
30+
internal fun FpsCounter(modifier: Modifier = Modifier, dispatcher: CoroutineDispatcher = Dispatchers.Default) {
31+
var displayedFPS by remember { mutableIntStateOf(0) }
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 textContent by remember {
37+
derivedStateOf {
38+
when (fpsCountMethod) {
39+
FPSCountMethod.RealTime -> {
40+
"FPS(Realtime):$displayedFPS"
41+
}
42+
43+
FPSCountMethod.FixedInterval -> {
44+
"FPS(last ${FPS_UPDATE_DELAY}ms):$displayedFPS"
45+
}
46+
47+
FPSCountMethod.FixedFrameCount -> {
48+
"FPS(last ${FRAME_COUNT} frames):$displayedFPS"
49+
}
50+
}
51+
}
52+
}
53+
54+
val minMaxContent by remember { derivedStateOf { "min:$minFps, max:$maxFps" } }
55+
val textColor by remember { derivedStateOf { displayedFPS.fpsCountColor } }
56+
57+
Row(modifier = modifier, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
58+
Text(
59+
text = textContent,
60+
modifier =
61+
Modifier.clickable(interactionSource = remember { MutableInteractionSource() }, indication = null) {
62+
fpsCountMethod =
63+
when (fpsCountMethod) {
64+
FPSCountMethod.FixedInterval -> FPSCountMethod.FixedFrameCount
65+
FPSCountMethod.FixedFrameCount -> FPSCountMethod.RealTime
66+
FPSCountMethod.RealTime -> FPSCountMethod.FixedInterval
67+
}
68+
minFps = 240
69+
maxFps = 0
70+
},
71+
color = textColor,
72+
)
73+
74+
Text(
75+
text = minMaxContent,
76+
modifier =
77+
Modifier.clickable(interactionSource = remember { MutableInteractionSource() }, indication = null) {
78+
minFps = 240
79+
maxFps = 0
80+
},
81+
)
82+
}
83+
84+
LaunchedEffect(Unit) {
85+
val fpsArray = FloatArray(FRAME_COUNT) { 0f }
86+
val fpsCount = AtomicInteger(0)
87+
val writeIndex = AtomicInteger(0)
88+
val lastWriteIndex = AtomicInteger(0)
89+
val lastUpdTime = AtomicLong(withFrameMillis { it })
90+
91+
launch(dispatcher) {
92+
while (true) {
93+
delay(FPS_UPDATE_DELAY)
94+
95+
displayedFPS =
96+
when (fpsCountMethod) {
97+
FPSCountMethod.FixedInterval -> fpsCount.getAndSet(0) * 1000 / FPS_UPDATE_DELAY.toInt()
98+
FPSCountMethod.FixedFrameCount -> fpsArray.average().roundToInt()
99+
FPSCountMethod.RealTime -> fpsArray[lastWriteIndex.get()].roundToInt()
100+
}
101+
if (displayedFPS > 0) {
102+
minFps = minOf(minFps, displayedFPS)
103+
}
104+
maxFps = maxOf(maxFps, displayedFPS)
105+
}
106+
}
107+
108+
while (true) {
109+
withFrameMillis { frameTimeMillis ->
110+
fpsCount.getAndUpdate { it + 1 }
111+
112+
fpsArray[writeIndex.get()] = 1000f / (frameTimeMillis - lastUpdTime.getAndSet(frameTimeMillis))
113+
114+
lastWriteIndex.set(writeIndex.getAndUpdate { (it + 1) % fpsArray.size })
115+
}
116+
}
117+
}
118+
}
119+
120+
private const val FPS_UPDATE_DELAY = 250L
121+
private const val FRAME_COUNT = 20
122+
123+
private val Int.fpsCountColor: Color
124+
get() =
125+
when {
126+
this > 55 -> Color.Green
127+
this > 45 -> Color.Yellow
128+
else -> Color.Red
129+
}
130+
131+
internal enum class FPSCountMethod {
132+
FixedInterval,
133+
FixedFrameCount,
134+
RealTime,
135+
}

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)