Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ import kotlin.time.Duration
* [UPathSelector] implementation which uses strategy similar to Linux O(1) scheduler
* to switch between states with different [Key]s (for example, different entry point methods).
* Keys are switched in round-robin fashion (so, all keys are guaranteed to be selected). Each key is given
* an equal time quantum which is calculated using the value returned by [getRemainingTime]. If such quantum is
* exceeded by some key, quantum for the remaining keys in round is recalculated (decreased equally). From other side,
* if some key takes less time than the quantum, another round is likely to happen.
* an equal time quantum calculated from [getRemainingTime] and bounded by [maxTimeQuantum]. An uncapped overrun is
* redistributed over the remaining keys; capped turns recompute their share of the actual remaining budget.
* If a key finishes before its quantum ends, there may be time for another round.
* Spent time is estimated as timespan between successive peeks. Keys in queue are sorted by [KeyPriority] and total time spent.
*
* @param initialKeys complete set of [Key]s which will be used with this instance. Operations with states having other keys
Expand All @@ -22,14 +22,16 @@ import kotlin.time.Duration
* @param getKeyPriority returns priority by key. Priority can change over time. Smaller values have greater priority.
* @param basePathSelectorFactory function to create a [UPathSelector] associated with specific key. States
* with the same key are maintained in path selector created by this function.
* @param maxTimeQuantum upper bound for each turn. It cannot interrupt an in-flight interpreter step.
*/
class ConstantTimeFairPathSelector<State, Key, KeyPriority : Comparable<KeyPriority>>(
initialKeys: Set<Key>,
private val stopwatch: Stopwatch,
private val getRemainingTime: () -> Duration,
getKey: (State) -> Key,
private val getKeyPriority: (Key) -> KeyPriority,
basePathSelectorFactory: (Key) -> UPathSelector<State>
basePathSelectorFactory: (Key) -> UPathSelector<State>,
private val maxTimeQuantum: Duration = Duration.INFINITE,
) : KeyedPathSelector<State, Key>(getKey) {

private data class KeysQueueElement<Key, KeyPriority>(val key: Key, var priority: KeyPriority, var elapsed: Duration)
Expand All @@ -44,16 +46,19 @@ class ConstantTimeFairPathSelector<State, Key, KeyPriority : Comparable<KeyPrior

init {
require(initialKeys.any()) { "initialKeys must contain at least one element" }
require(maxTimeQuantum > Duration.ZERO) { "maxTimeQuantum must be positive" }
stopwatch.reset()
initialKeys.forEach {
pathSelectors[it] = basePathSelectorFactory(it)
activeQueue.add(KeysQueueElement(it, getKeyPriority(it), Duration.ZERO))
}
val pathSelectorsCount = pathSelectors.size
val remainingTime = getRemainingTime()
currentTimeQuantum = remainingTime / pathSelectorsCount
currentTimeQuantum = calculateTimeQuantum(pathSelectorsCount)
}

private fun calculateTimeQuantum(keysCount: Int): Duration =
minOf(getRemainingTime() / keysCount, maxTimeQuantum)

private fun peekFromQueues(): KeysQueueElement<Key, KeyPriority> {
check(activeQueue.isNotEmpty() || expiredList.isNotEmpty()) { "Trying to peek from empty path selector" }
if (activeQueue.isEmpty()) {
Expand All @@ -62,8 +67,7 @@ class ConstantTimeFairPathSelector<State, Key, KeyPriority : Comparable<KeyPrior
activeQueue.add(it)
}
expiredList.clear()
val remainingTime = getRemainingTime()
currentTimeQuantum = remainingTime / activeQueue.size
currentTimeQuantum = calculateTimeQuantum(activeQueue.size)
}
return activeQueue.peek()
}
Expand Down Expand Up @@ -91,7 +95,11 @@ class ConstantTimeFairPathSelector<State, Key, KeyPriority : Comparable<KeyPrior
pathSelectors.remove(currentKey.key)
}
if (stopwatch.elapsed > currentTimeQuantum && activeQueue.isNotEmpty()) {
currentTimeQuantum -= (stopwatch.elapsed - currentTimeQuantum) / activeQueue.size
currentTimeQuantum = if (maxTimeQuantum.isFinite()) {
calculateTimeQuantum(activeQueue.size)
} else {
currentTimeQuantum - (stopwatch.elapsed - currentTimeQuantum) / activeQueue.size
}
}
currentKey = peekFromQueues()
currentPathSelector = pathSelectors[currentKey.key]
Expand Down
29 changes: 21 additions & 8 deletions usvm-core/src/main/kotlin/org/usvm/ps/PathSelectorFactory.kt
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,10 @@ fun <Method, Statement, Target, State> createPathSelector(
callGraphStatisticsFactory: () -> CallGraphStatistics<Method>? = { null },
loopStatisticFactory: () -> StateLoopTracker<*, Statement, State>? = { null },
): UPathSelector<State> where Target : UTarget<Statement, Target>, State : UState<*, Method, Statement, *, Target, State> {
if (options.timeout == Duration.INFINITE || initialStates.size == 1) {
val hasSingleEntryPoint = initialStates.size == 1
val hasUnlimitedScheduling = options.timeout == Duration.INFINITE &&
options.maxEntryPointTimeQuantum == Duration.INFINITE
if (hasSingleEntryPoint || hasUnlimitedScheduling) {
return createPathSelector(
initialStates.values.toList(),
options,
Expand Down Expand Up @@ -209,16 +212,22 @@ fun <Method, Statement, Target, State> createPathSelector(
val pathSelector = when (options.pathSelectorFairnessStrategy) {
PathSelectorFairnessStrategy.CONSTANT_TIME -> {
val getMethodCoverage = coverageStatistics?.let {
{ m: Method -> coverageStatistics.getMethodCoverage(m) }
val trackedMethods = coverageStatistics.coverageZone.toHashSet()
val entryPointCoverage: (Method) -> Float = { method ->
if (method in trackedMethods) coverageStatistics.getMethodCoverage(method) else 0f
}

entryPointCoverage
} ?: { 0f }

ConstantTimeFairPathSelector(
initialStates.keys,
RealTimeStopwatch(),
::getRemainingTimeMs,
{ it.entrypoint },
getMethodCoverage,
::createBasePathSelector
initialKeys = initialStates.keys,
stopwatch = RealTimeStopwatch(),
getRemainingTime = ::getRemainingTimeMs,
getKey = { it.entrypoint },
getKeyPriority = getMethodCoverage,
basePathSelectorFactory = ::createBasePathSelector,
maxTimeQuantum = options.maxEntryPointTimeQuantum,
)
}

Expand All @@ -231,6 +240,10 @@ fun <Method, Statement, Target, State> createPathSelector(
)
}

if (!options.removeCoveredEntryPoints) {
return pathSelector
}

val totalCoveragePercents = 100f
when (options.coverageZone) {
CoverageZone.METHOD -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,17 @@ class StatisticsByMethodPrinter<Method, Statement, State : UState<*, Method, Sta
if (methods.isEmpty()) {
return
}

val trackedMethods = coverageStatistics.coverageZone.toHashSet()
val statsStrings = mutableListOf(StatisticsRow("Method", "Coverage, %", "Time spent, ms", "Steps"))
methods.forEach {
val name = getMethodSignature(it)
val coverage = coverageStatistics.getMethodCoverage(it).roundToInt().toString()
val coverage = if (it in trackedMethods) {
coverageStatistics.getMethodCoverage(it).roundToInt().toString()
} else {
"N/A"
}

val time = timeStatistics.getTimeSpentOnMethod(it).inWholeMilliseconds.toString()
val stepsCount = stepsStatistics.getMethodSteps(it).toString()
statsStrings.add(StatisticsRow(name, coverage, time, stepsCount))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
package org.usvm.ps

import org.junit.jupiter.api.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds

class ConstantTimeFairPathSelectorQuantumTest {
@Test
fun `bounded turns revisit every key and retain its pending states`() {
val stopwatch = TestStopwatch()
var remainingSeconds = 60L
val selector = selector(
stopwatch = stopwatch,
remainingTime = { remainingSeconds.seconds },
maxTimeQuantum = 2.seconds,
)
selector.add(states = listOf("a1", "a2", "b1", "c1"))

val visited = List(size = 12) { index ->
val state = selector.peek()
if (index == 1) {
selector.remove(state = state)
}
stopwatch.elapsedRaw++
remainingSeconds--
state
}

assertEquals(
expected = listOf("a1", "a1", "b1", "b1", "c1", "c1", "a2", "a2", "b1", "b1", "c1", "c1"),
actual = visited,
)
}

@Test
fun `an overrun preserves bounded turns for the remaining keys`() {
val stopwatch = TestStopwatch()
var remainingSeconds = 100L
val selector = selector(
stopwatch = stopwatch,
remainingTime = { remainingSeconds.seconds },
maxTimeQuantum = 2.seconds,
)
selector.add(states = listOf("a1", "b1", "c1"))
assertEquals(expected = "a1", actual = selector.peek())

stopwatch.elapsedRaw = 20L
remainingSeconds = 80L
val afterOverrun = selector.peek()
stopwatch.elapsedRaw = 1L
remainingSeconds = 79L
val withinNextTurn = selector.peek()
stopwatch.elapsedRaw = 2L
remainingSeconds = 78L
val afterNextTurn = selector.peek()

assertEquals(expected = "b1", actual = afterOverrun)
assertEquals(expected = "b1", actual = withinNextTurn)
assertEquals(expected = "c1", actual = afterNextTurn)
}

@Test
fun `the remaining budget can shorten the configured turn`() {
val stopwatch = TestStopwatch()
var remainingSeconds = 3L
val selector = selector(
stopwatch = stopwatch,
remainingTime = { remainingSeconds.seconds },
maxTimeQuantum = 2.seconds,
)
selector.add(states = listOf("a1", "b1", "c1"))

val visited = List(size = 3) {
val state = selector.peek()
stopwatch.elapsedRaw++
remainingSeconds--
state
}

assertEquals(expected = listOf("a1", "b1", "c1"), actual = visited)
}

@Test
fun `a finite turn also works with an unlimited total budget`() {
val stopwatch = TestStopwatch()
val selector = selector(
stopwatch = stopwatch,
remainingTime = { Duration.INFINITE },
maxTimeQuantum = 2.seconds,
)
selector.add(states = listOf("a1", "b1", "c1"))
assertEquals(expected = "a1", actual = selector.peek())

stopwatch.elapsedRaw = 2L
val nextState = selector.peek()

assertEquals(expected = "b1", actual = nextState)
}

@Test
fun `nonpositive turn bounds are rejected`() {
for (quantum in listOf(Duration.ZERO, -1.seconds)) {
val stopwatch = TestStopwatch()

assertFailsWith<IllegalArgumentException> {
selector(
stopwatch = stopwatch,
remainingTime = { 30.seconds },
maxTimeQuantum = quantum,
)
}
}
}

private fun selector(
stopwatch: TestStopwatch,
remainingTime: () -> Duration,
maxTimeQuantum: Duration,
): ConstantTimeFairPathSelector<String, Char, Char> = ConstantTimeFairPathSelector(
initialKeys = setOf('a', 'b', 'c'),
stopwatch = stopwatch,
getRemainingTime = remainingTime,
getKey = { state -> state.first() },
getKeyPriority = { key -> key },
basePathSelectorFactory = { BfsPathSelector() },
maxTimeQuantum = maxTimeQuantum,
)
}
34 changes: 33 additions & 1 deletion usvm-ts/src/main/kotlin/org/usvm/machine/TsMachine.kt
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,12 @@ class TsMachine(
unknownCallDispatcher: TsUnknownCallDispatcher? = null,
unknownCallModels: TsUnknownCallModelCatalog? = null,
) : UMachine<TsState>() {
init {
val hasCompatibleCoverageScope = tsOptions.classCoverageTargets == null ||
options.coverageZone == CoverageZone.CLASS
require(hasCompatibleCoverageScope) { "Explicit class coverage targets require CoverageZone.CLASS" }
}

private val resolvedUnknownCallModels = when {
unknownCallDispatcher != null -> null
unknownCallModels != null -> unknownCallModels
Expand Down Expand Up @@ -169,7 +175,7 @@ class TsMachine(
val methodsToTrackCoverage =
when (options.coverageZone) {
CoverageZone.METHOD, CoverageZone.TRANSITIVE -> initialStates.keys.toHashSet()
CoverageZone.CLASS -> TODO("Unsupported yet")
CoverageZone.CLASS -> classCoverageMethods(entryMethods = initialStates.keys)
}

val coverageStatistics = CoverageStatistics<EtsMethod, EtsStmt, TsState>(
Expand Down Expand Up @@ -290,6 +296,32 @@ class TsMachine(
)
}

private fun classCoverageMethods(entryMethods: Set<EtsMethod>): Set<EtsMethod> {
val targets = tsOptions.classCoverageTargets
val classes = if (targets == null) {
entryMethods.map { method ->
requireNotNull(method.enclosingClass) {
"Class coverage requires a declaring class for ${method.signature}"
}
}
} else {
val classesBySignature = runtimeScene.projectAndSdkClasses.groupBy { owner -> owner.signature }
val unresolvedTargets = targets.filter { target -> classesBySignature[target]?.size != 1 }
require(unresolvedTargets.isEmpty()) {
"Class coverage targets must identify one scene class each: $unresolvedTargets"
}

targets.map { target -> classesBySignature.getValue(target).single() }
}
val coveredMethods = classes.flatMapTo(hashSetOf()) { owner ->
owner.methods.filter { candidate ->
candidate.signature.enclosingClass == owner.signature && candidate.cfg.stmts.isNotEmpty()
}
}

return if (targets == null) coveredMethods + entryMethods else coveredMethods
}

private fun emptyAnalysisResult(unsupportedPaths: Set<String>) = TsMachineAnalysisResult(
states = emptyList(),
stopReason = TsAnalysisStopReason.EXHAUSTED,
Expand Down
6 changes: 6 additions & 0 deletions usvm-ts/src/main/kotlin/org/usvm/machine/TsOptions.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.usvm.machine

import org.jacodb.ets.model.EtsClassSignature
import org.usvm.machine.call.TsResidualCallPolicy
import org.usvm.machine.call.TsUnknownCallModelSelection

Expand Down Expand Up @@ -29,8 +30,13 @@ data class TsOptions(
val dateNowMilliseconds: Double? = null,
/** Erase TypeScript assertions; false retains the existing ETS cast constraints and unsupported casts. */
val eraseTypeAssertions: Boolean = false,
/** CLASS coverage targets for wrapper entry points; null uses the entry points' declaring classes. */
val classCoverageTargets: Set<EtsClassSignature>? = null,
) {
init {
val hasCoverageTargets = classCoverageTargets == null || classCoverageTargets.isNotEmpty()
require(hasCoverageTargets) { "Explicit class coverage targets must not be empty" }

val isValidDateNow = dateNowMilliseconds == null ||
dateNowMilliseconds.isFinite() &&
dateNowMilliseconds % 1.0 == 0.0 &&
Expand Down
Loading
Loading