Skip to content

Commit

Permalink
WIP: fixing bounds adjustment formula
Browse files Browse the repository at this point in the history
Signed-off-by: Evgenii Moiseenko <[email protected]>
  • Loading branch information
eupp committed May 15, 2023
1 parent 0861687 commit 3c532a2
Show file tree
Hide file tree
Showing 3 changed files with 61 additions and 47 deletions.
78 changes: 44 additions & 34 deletions src/jvm/main/org/jetbrains/kotlinx/lincheck/Planner.kt
Original file line number Diff line number Diff line change
Expand Up @@ -187,20 +187,20 @@ internal class AdaptivePlanner(
}

override fun shouldDoNextIteration(iteration: Int): Boolean {
check(iteration == statisticsTracker.iteration)
if (iteration >= WARM_UP_ITERATIONS.coerceAtLeast(1) && iteration % BOUNDS_ADJUSTMENT_INTERVAL == 0) {
check(iteration == statisticsTracker.iteration + 1)
if (iteration >= WARM_UP_ITERATIONS.coerceAtLeast(1) /* && iteration % BOUNDS_ADJUSTMENT_INTERVAL == 0 */) {
adjustBounds(
currentIteration = iteration,
averageInvocationsCount = statisticsTracker.averageInvocationsCount(iteration),
averageInvocationTimeNano = statisticsTracker.averageInvocationTimeNano(iteration - 1),
performedIterations = statisticsTracker.iteration + 1,
performedInvocations = statisticsTracker.iterationsInvocationsCount.sum(),
averageInvocationTimeNano = statisticsTracker.averageInvocationTimeNano(statisticsTracker.iteration),
remainingTimeNano = remainingTimeNano,
)
}
return (remainingTimeNano > 0) && (iteration < iterationsBound)
}

override fun shouldDoNextInvocation(invocation: Int): Boolean {
check(invocation == statisticsTracker.invocation)
check(invocation == statisticsTracker.invocation + 1)
return (remainingTimeNano > 0) && (invocation < invocationsBound)
}

Expand Down Expand Up @@ -228,21 +228,26 @@ internal class AdaptivePlanner(
* (this can happen, for example, when we hit invocation max/min bounds).
*/
private fun adjustBounds(
currentIteration: Int,
averageInvocationsCount: Double,
performedIterations: Int,
performedInvocations: Int,
averageInvocationTimeNano: Double,
remainingTimeNano: Long
) {
require(averageInvocationTimeNano > 0)
if (remainingTimeNano <= 0)
return
// calculate invocation and iteration bounds
val averageInvocationsCount = performedInvocations.toDouble() / performedIterations
// estimate number of remaining invocations
val remainingInvocations = floor(remainingTimeNano / averageInvocationTimeNano)
// shorter name for invocations to iterations ratio constant
val ratio = INVOCATIONS_TO_ITERATIONS_RATIO.toDouble()
// calculate remaining iterations bound
var remainingIterations = solveQuadraticEquation(
a = INVOCATIONS_TO_ITERATIONS_RATIO.toDouble(),
b = INVOCATIONS_TO_ITERATIONS_RATIO * currentIteration - averageInvocationsCount,
c = -remainingInvocations
).let { floor(it).toInt() }
a = ratio,
b = 2 * ratio * performedIterations,
c = ratio * performedIterations * performedIterations - (performedInvocations + remainingInvocations)
).let { floor(it).toInt() }.coerceAtLeast(0)
// derive invocations per iteration bound
invocationsBound = if (remainingIterations > 0) {
round(remainingInvocations / remainingIterations)
.toInt()
Expand All @@ -266,47 +271,49 @@ internal class AdaptivePlanner(
}
remainingIterations = remainingIterations.coerceAtLeast(0)

// if there is no remaining iterations, but there is still some time left,
// if there is no remaining iterations, but there is still some time left
// (more time than admissible error),
// we still can try to perform one more iteration ---
// in case of overdue it will be just aborted when the time is up;
// this additional iteration helps us to prevent the case when we finish earlier and
// do not use all allocated testing time;
// however, because in some rare cases even single invocation can take significant time
// and thus surpass the deadline, we still perform additional check
// to see if there enough time to perform at least L additional invocations.
if (remainingIterations == 0 && averageInvocationTimeNano * INVOCATIONS_DELTA < remainingTimeNano)
if (remainingIterations == 0 &&
remainingTimeNano > TIME_ERROR_MARGIN_NANO &&
averageInvocationTimeNano * INVOCATIONS_DELTA < remainingTimeNano) {
remainingIterations += 1
}

// finally, set the iterations bound
iterationsBound = currentIteration + remainingIterations
iterationsBound = performedIterations + remainingIterations

val currentRatio = averageInvocationsCount / currentIteration
val estimatedRatio = (averageInvocationsCount + invocationsBound) / iterationsBound
val currentRatio = averageInvocationsCount / performedIterations
val estimatedRatio = (performedInvocations + invocationsBound * remainingIterations) / (iterationsBound * iterationsBound).toDouble()
val nextRatio = (averageInvocationsCount + invocationsBound) / (currentRatio + 1)

println("currentIteration=$currentIteration")
println("remainingIterations=${remainingIterations}, remainingTime=${nanoTimeToString(remainingTimeNano)}")
println("iterationsBound=$iterationsBound, invocationsBound=$invocationsBound")
println("currentRatio=${String.format("%.3f", currentRatio)}")
println("estimatedRatio=${String.format("%.3f", estimatedRatio)}")
println("nextRatio=${String.format("%.3f", nextRatio)}")
println()
println("averageInvocationsCount=${String.format("%.3f", averageInvocationsCount)}")
println("averageInvocationTime=${nanoTimeToString(averageInvocationTimeNano.toLong(), decimalPlaces = 9)}")
println("iterationTimeEstimate=${nanoTimeToString(iterationTimeEstimateNano.toLong())}")
println("remainingTimeEstimate=${nanoTimeToString(remainingTimeEstimateNano.toLong())}")
println("iterationsDiff=$iterationsDiff, timeDiffNano=${nanoTimeToString(timeDiffNano.toLong())}")
// println("performedIterations=$performedIterations")
// println("remainingIterations=${remainingIterations}, remainingTime=${nanoTimeToString(remainingTimeNano)}")
// println("iterationsBound=$iterationsBound, invocationsBound=$invocationsBound")
// println("currentRatio=${String.format("%.3f", currentRatio)}")
// println("estimatedRatio=${String.format("%.3f", estimatedRatio)}")
// println("nextRatio=${String.format("%.3f", nextRatio)}")
// println()
// println("averageInvocationsCount=${String.format("%.3f", averageInvocationsCount)}")
// println("averageInvocationTime=${nanoTimeToString(averageInvocationTimeNano.toLong(), decimalPlaces = 9)}")
// println("iterationTimeEstimate=${nanoTimeToString(iterationTimeEstimateNano.toLong())}")
// println("remainingTimeEstimate=${nanoTimeToString(remainingTimeEstimateNano.toLong())}")
// println("iterationsDiff=$iterationsDiff, timeDiffNano=${nanoTimeToString(timeDiffNano.toLong())}")
}

private fun solveQuadraticEquation(a: Double, b: Double, c: Double): Double {
val d = (b * b - 4 * a * c)
check(d >= 0)
val r = max(
return max(
(-b + sqrt(d)) / (2 * a),
(-b - sqrt(d)) / (2 * a),
)
check(r >= 0)
return r
}

companion object {
Expand All @@ -329,11 +336,14 @@ internal class AdaptivePlanner(
internal const val INVOCATIONS_TO_ITERATIONS_RATIO = 100

internal const val WARM_UP_ITERATIONS = 3
internal const val BOUNDS_ADJUSTMENT_INTERVAL = 5
// internal const val BOUNDS_ADJUSTMENT_INTERVAL = 5

internal const val INVOCATIONS_LOWER_BOUND = 100
internal const val STRESS_INVOCATIONS_UPPER_BOUND = 1_000_000
internal const val MODEL_CHECKING_INVOCATIONS_UPPER_BOUND = 20_000

// error up to 1 sec --- we can try to decrease the error in the future
internal const val TIME_ERROR_MARGIN_NANO = 1_000_000_000
}

}
Expand Down
25 changes: 15 additions & 10 deletions src/jvm/main/org/jetbrains/kotlinx/lincheck/Statistics.kt
Original file line number Diff line number Diff line change
Expand Up @@ -89,23 +89,23 @@ class StatisticsTracker : Statistics {

override val iterationsRunningTimeNano: List<Long>
get() = _iterationsRunningTimeNano
private val _iterationsRunningTimeNano = mutableListOf(0L)
private val _iterationsRunningTimeNano = mutableListOf<Long>()

override val iterationsInvocationsCount: List<Int>
get() = _iterationsInvocationCount
private val _iterationsInvocationCount = mutableListOf(0)
private val _iterationsInvocationCount = mutableListOf<Int>()

/**
* Current iteration number.
*/
val iteration: Int
get() = iterationsRunningTimeNano.lastIndex
var iteration: Int = -1
private set

/**
* Current invocation number within current iteration.
*/
val invocation: Int
get() = iterationsInvocationsCount[iteration]
var invocation: Int = -1
private set

/**
* Running time of current iteration.
Expand All @@ -119,14 +119,19 @@ class StatisticsTracker : Statistics {
val currentIterationInvocationsCount: Int
get() = iterationsInvocationsCount[iteration]

fun iterationStart() {}

fun iterationEnd() {
fun iterationStart() {
++iteration
_iterationsRunningTimeNano.add(0)
_iterationsInvocationCount.add(0)
}

fun invocationStart() {}
fun iterationEnd() {
invocation = -1
}

fun invocationStart() {
++invocation
}

fun invocationEnd(invocationTimeNano: Long) {
runningTimeNano += invocationTimeNano
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,16 +81,15 @@ abstract class AbstractLincheckTest(
val statistics = result.statistics
val randomTestingTimeNano = testingTimeInSeconds * 1_000_000_000
val runningTimeNano = statistics.runningTimeNano
// error up to 1 sec --- we can try to decrease the error in the future;
val timeDeltaNano = 1_000_000_000
val timeDeltaNano = AdaptivePlanner.TIME_ERROR_MARGIN_NANO
// check that the actual running time is close to specified time
assert(abs(randomTestingTimeNano - runningTimeNano) < timeDeltaNano) { """
Testing time is beyond expected bounds:
actual: ${String.format("%.3f", runningTimeNano.toDouble() / 1_000_000_000)}
expected: ${String.format("%.3f", randomTestingTimeNano.toDouble() / 1_000_000_000)}
""".trimIndent()
}
// check invocations / iterations ratio
// check invocations to iterations ratio
val plannedIterations = statistics.iterationsInvocationsCount
if (plannedIterations.isEmpty())
return
Expand Down

0 comments on commit 3c532a2

Please sign in to comment.