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
import java.util.*
import java.awt.Component
import kotlin.math.roundToInt
internal class FPSCounter(
private val count: Int,
private val probability: Double
private val periodSeconds: Double,
private val showLongFrames: Boolean,
private val getLongFrameMillis: () -> Double
) {
private var i = 0
private val times = LinkedList<Double>()
private var t1 = 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]
}
private val times = mutableListOf<Long>()
private var lastLogTime = System.nanoTime()
private var lastTime = System.nanoTime()
fun tick() {
val t2 = System.nanoTime()
val frameTime = (t2 - t1) / 1E6
t1 = t2
val time = System.nanoTime()
val timestamp = time.nanosToMillis().toLong()
val frameTime = time - lastTime
lastTime = time
i++
times.add(frameTime)
if (times.size > count) {
times.removeFirst()
if (showLongFrames && frameTime > getLongFrameMillis().millisToNanos()) {
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) {
val quantile = (1 - probability) / 2.0
val average = (1000.0 / times.average()).roundToInt()
val min = (1000.0 / times.quantile(1 - quantile)).roundToInt()
val max = (1000.0 / times.quantile(quantile)).roundToInt()
val probability = (100 * probability).roundToInt()
println("FPS $average ($min-$max $probability%)")
private val nanosPerMillis = 1_000_000.0
private val nanosPerSecond = 1_000_000_000.0
private fun Long.nanosToMillis(): Double = this / nanosPerMillis
private fun Double.millisToNanos(): Long = (this * nanosPerMillis).toLong()
private fun Double.secondsToNanos(): Long = (this * nanosPerSecond).toLong()
}
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
import org.jetbrains.skija.*
import org.jetbrains.skiko.redrawer.Redrawer
import org.jetbrains.skiko.redrawer.RasterRedrawer
import org.jetbrains.skiko.context.createContextHandler
import org.jetbrains.skija.Canvas
import org.jetbrains.skija.ClipMode
import org.jetbrains.skija.Picture
import org.jetbrains.skija.PictureRecorder
import org.jetbrains.skija.Rect
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 javax.swing.SwingUtilities.isEventDispatchThread
......@@ -64,18 +68,14 @@ open class SkiaLayer : HardwareLayer() {
redrawer?.needRedraw()
}
private val fpsCounter = FPSCounter(
count = SkikoProperties.fpsCount,
probability = SkikoProperties.fpsProbability
)
@Suppress("LeakingThis")
private val fpsCounter = defaultFPSCounter(this)
override fun update(nanoTime: Long) {
check(!isDisposed)
check(isEventDispatchThread())
if (SkikoProperties.fpsEnabled) {
fpsCounter.tick()
}
fpsCounter?.tick()
val pictureWidth = (width * contentScale).toInt().coerceAtLeast(0)
val pictureHeight = (height * contentScale).toInt().coerceAtLeast(0)
......
......@@ -5,8 +5,13 @@ internal object SkikoProperties {
val vsyncEnabled: Boolean by property("skiko.vsync.enabled", default = true)
val fpsEnabled: Boolean by property("skiko.fps.enabled", default = false)
val fpsCount: Int by property("skiko.fps.count", default = 300)
val fpsProbability: Double by property("skiko.fps.probability", default = 0.97)
val fpsPeriodSeconds: Double by property("skiko.fps.periodSeconds", default = 2.0)
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 environment = System.getenv("SKIKO_RENDER_API")
......@@ -30,11 +35,11 @@ internal object SkikoProperties {
System.getProperty(name)?.toBoolean() ?: default
}
private fun property(name: String, default: Int) = lazy {
System.getProperty(name)?.toInt() ?: default
private fun property(name: String, default: Double) = lazy {
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
}
}
\ 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