blob: 3cabb529d626c241a38c122a2ba57b4e85b8154e [file] [log] [blame]
/*
* Copyright 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.benchmark
import android.util.Log
import java.util.concurrent.TimeUnit
/**
* Used to detect when a benchmark has warmed up, given time taken for each iteration.
*
* Uses emperically determined constants, primarily looking for the convergence of two
* exponential moving averages.
*
* Tuned to do minimal amount of intrusive work in onNextIteration to avoid polluting the benchmark.
*/
internal class WarmupManager {
private var fastMovingAvg: Float = 0f
private var slowMovingAvg: Float = 0f
private var similarIterationCount: Int = 0
val estimatedIterationTimeNs: Float get() = fastMovingAvg
var iteration = 0
private set
var totalDurationNs: Long = 0
private set
/**
* Pass the just-run iteration timing, and return whether the warmup has completed.
*
* NOTE: it is critical to do a minimum amount of work and memory access in this method, to
* avoid polluting the benchmark's memory access patterns. This is why we chose exponential
* moving averages, and why we only log once at the end.
*
* @param durationNs Duration of the next iteration.
* @return True if the warmup has completed, false otherwise.
*/
fun onNextIteration(durationNs: Long): Boolean {
iteration++
totalDurationNs += durationNs
if (iteration == 1) {
fastMovingAvg = durationNs.toFloat()
slowMovingAvg = durationNs.toFloat()
return false
}
fastMovingAvg = FAST_RATIO * durationNs + (1 - FAST_RATIO) * fastMovingAvg
slowMovingAvg = SLOW_RATIO * durationNs + (1 - SLOW_RATIO) * slowMovingAvg
// If fast moving avg is close to slow, the benchmark is stabilizing
val ratio = fastMovingAvg / slowMovingAvg
if (ratio < 1 + THRESHOLD && ratio > 1 - THRESHOLD) {
similarIterationCount++
} else {
similarIterationCount = 0
}
if (iteration >= MIN_ITERATIONS && totalDurationNs >= MIN_DURATION_NS) {
if (similarIterationCount > MIN_SIMILAR_ITERATIONS ||
totalDurationNs >= MAX_DURATION_NS
) {
// benchmark has stabilized, or we're out of time
return true
}
}
return false
}
fun logInfo() {
if (iteration > 0) {
Log.d(
BenchmarkState.TAG,
"Warmup: t=%.3f, iter=%d, fastAvg=%3.0f, slowAvg=%3.0f"
.format(
totalDurationNs / 1e9,
iteration,
fastMovingAvg,
slowMovingAvg
)
)
}
}
companion object {
val MIN_DURATION_NS = TimeUnit.MILLISECONDS.toNanos(250)
val MAX_DURATION_NS = TimeUnit.SECONDS.toNanos(8)
const val MIN_ITERATIONS = 30
private const val MIN_SIMILAR_ITERATIONS = 40
private const val FAST_RATIO = 0.1f
private const val SLOW_RATIO = 0.005f
private const val THRESHOLD = 0.04f
}
}