Commit a29b62e4 authored by Igor Demin's avatar Igor Demin

Rewrite FPS counting

- property `skiko.fps.longFrames.show` to show frames longer than display refresh rate
- replace `skiko.fps.count` by `skiko.fps.periodSeconds`
- log timestamp
- remove `skiko.fps.probability`. min/max FPS are more informative
parent f6f67b81
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="SkijaInjectSample (show long frames) " type="GradleRunConfiguration" factoryName="Gradle">
<ExternalSystemSettings>
<option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$/samples/SkijaInjectSample" />
<option name="externalSystemIdString" value="GRADLE" />
<option name="scriptParameters" value="-Dskiko.fps.longFrames.show=true" />
<option name="taskDescriptions">
<list />
</option>
<option name="taskNames">
<list>
<option value="run" />
</list>
</option>
<option name="vmOptions" value="" />
</ExternalSystemSettings>
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
<DebugAllEnabled>false</DebugAllEnabled>
<method v="2">
<option name="Gradle.BeforeRunTask" enabled="false" tasks="publishToMavenLocal" externalProjectPath="$PROJECT_DIR$/skiko" vmOptions="" scriptParameters="" />
</method>
</configuration>
</component>
\ No newline at end of file
package org.jetbrains.skiko package org.jetbrains.skiko
import java.util.* import java.awt.Component
import kotlin.math.roundToInt import kotlin.math.roundToInt
internal class FPSCounter( internal class FPSCounter(
private val count: Int, private val periodSeconds: Double,
private val probability: Double private val showLongFrames: Boolean,
private val getLongFrameMillis: () -> Double
) { ) {
private var i = 0 private val times = mutableListOf<Long>()
private val times = LinkedList<Double>() private var lastLogTime = System.nanoTime()
private var t1 = System.nanoTime() private var lastTime = System.nanoTime()
/**
* [value] 0.0 - min, 1.0 - max, 0.5 - median
*/
private fun MutableList<Double>.quantile(value: Double) : Double {
val index = (value * (size - 1)).toInt()
return sorted()[index]
}
fun tick() { fun tick() {
val t2 = System.nanoTime() val time = System.nanoTime()
val frameTime = (t2 - t1) / 1E6 val timestamp = time.nanosToMillis().toLong()
t1 = t2 val frameTime = time - lastTime
lastTime = time
i++
times.add(frameTime) times.add(frameTime)
if (times.size > count) { if (showLongFrames && frameTime > getLongFrameMillis().millisToNanos()) {
times.removeFirst() println("[%d] Long frame %.2f ms".format(timestamp, frameTime.nanosToMillis()))
}
if ((time - lastLogTime) > periodSeconds.secondsToNanos()) {
val average = (nanosPerSecond / times.average()).roundToInt()
val min = (nanosPerSecond / times.max()!!).roundToInt()
val max = (nanosPerSecond / times.min()!!).roundToInt()
println("[$timestamp] FPS $average ($min-$max)")
times.clear()
lastLogTime = time
}
} }
if (i % count == 0) { private val nanosPerMillis = 1_000_000.0
val quantile = (1 - probability) / 2.0 private val nanosPerSecond = 1_000_000_000.0
val average = (1000.0 / times.average()).roundToInt() private fun Long.nanosToMillis(): Double = this / nanosPerMillis
val min = (1000.0 / times.quantile(1 - quantile)).roundToInt() private fun Double.millisToNanos(): Long = (this * nanosPerMillis).toLong()
val max = (1000.0 / times.quantile(quantile)).roundToInt() private fun Double.secondsToNanos(): Long = (this * nanosPerSecond).toLong()
val probability = (100 * probability).roundToInt() }
println("FPS $average ($min-$max $probability%)")
internal fun defaultFPSCounter(
component: Component
): FPSCounter? = with(SkikoProperties) {
if (!SkikoProperties.fpsEnabled) return@with null
// it is slow on Linux (100ms), so we cache it. Also refreshRate available only after window is visible
val refreshRate by lazy { component.graphicsConfiguration.device.displayMode.refreshRate }
FPSCounter(
periodSeconds = fpsPeriodSeconds,
showLongFrames = fpsLongFramesShow
) {
when {
fpsLongFramesShow && fpsLongFramesMillis != null -> fpsLongFramesMillis!!
fpsLongFramesShow -> 1.5 * 1000 / refreshRate
else -> 0.0
} }
} }
} }
\ No newline at end of file
package org.jetbrains.skiko package org.jetbrains.skiko
import org.jetbrains.skija.* import org.jetbrains.skija.Canvas
import org.jetbrains.skiko.redrawer.Redrawer import org.jetbrains.skija.ClipMode
import org.jetbrains.skiko.redrawer.RasterRedrawer import org.jetbrains.skija.Picture
import org.jetbrains.skiko.context.createContextHandler import org.jetbrains.skija.PictureRecorder
import org.jetbrains.skija.Rect
import org.jetbrains.skiko.context.SoftwareContextHandler import org.jetbrains.skiko.context.SoftwareContextHandler
import org.jetbrains.skiko.context.createContextHandler
import org.jetbrains.skiko.redrawer.RasterRedrawer
import org.jetbrains.skiko.redrawer.Redrawer
import java.awt.Graphics import java.awt.Graphics
import javax.swing.SwingUtilities.isEventDispatchThread import javax.swing.SwingUtilities.isEventDispatchThread
...@@ -64,18 +68,14 @@ open class SkiaLayer : HardwareLayer() { ...@@ -64,18 +68,14 @@ open class SkiaLayer : HardwareLayer() {
redrawer?.needRedraw() redrawer?.needRedraw()
} }
private val fpsCounter = FPSCounter( @Suppress("LeakingThis")
count = SkikoProperties.fpsCount, private val fpsCounter = defaultFPSCounter(this)
probability = SkikoProperties.fpsProbability
)
override fun update(nanoTime: Long) { override fun update(nanoTime: Long) {
check(!isDisposed) check(!isDisposed)
check(isEventDispatchThread()) check(isEventDispatchThread())
if (SkikoProperties.fpsEnabled) { fpsCounter?.tick()
fpsCounter.tick()
}
val pictureWidth = (width * contentScale).toInt().coerceAtLeast(0) val pictureWidth = (width * contentScale).toInt().coerceAtLeast(0)
val pictureHeight = (height * contentScale).toInt().coerceAtLeast(0) val pictureHeight = (height * contentScale).toInt().coerceAtLeast(0)
......
...@@ -5,8 +5,13 @@ internal object SkikoProperties { ...@@ -5,8 +5,13 @@ internal object SkikoProperties {
val vsyncEnabled: Boolean by property("skiko.vsync.enabled", default = true) val vsyncEnabled: Boolean by property("skiko.vsync.enabled", default = true)
val fpsEnabled: Boolean by property("skiko.fps.enabled", default = false) val fpsEnabled: Boolean by property("skiko.fps.enabled", default = false)
val fpsCount: Int by property("skiko.fps.count", default = 300) val fpsPeriodSeconds: Double by property("skiko.fps.periodSeconds", default = 2.0)
val fpsProbability: Double by property("skiko.fps.probability", default = 0.97) val fpsLongFramesShow: Boolean by property("skiko.fps.longFrames.show", default = false)
/**
* If the property isn't defined, but skiko.fps.longFrames.show is true, this property will be 1.5 * (1000 / displayRefreshRate)
*/
val fpsLongFramesMillis: Double? by property("skiko.fps.longFrames.millis", default = null)
val renderApi: GraphicsApi by lazy { val renderApi: GraphicsApi by lazy {
val environment = System.getenv("SKIKO_RENDER_API") val environment = System.getenv("SKIKO_RENDER_API")
...@@ -30,11 +35,11 @@ internal object SkikoProperties { ...@@ -30,11 +35,11 @@ internal object SkikoProperties {
System.getProperty(name)?.toBoolean() ?: default System.getProperty(name)?.toBoolean() ?: default
} }
private fun property(name: String, default: Int) = lazy { private fun property(name: String, default: Double) = lazy {
System.getProperty(name)?.toInt() ?: default System.getProperty(name)?.toDouble() ?: default
} }
private fun property(name: String, default: Double) = lazy { private fun property(name: String, default: Double?) = lazy {
System.getProperty(name)?.toDouble() ?: default System.getProperty(name)?.toDouble() ?: default
} }
} }
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment