fix(sidecar): pollUntilStable stops charging the read to the stability streak

The doc has said since b02e86b that the streak is quiet the poll observed and
that a 500ms read must not clear a 750ms streak having watched 250ms. now was
sampled after snapshot() returns, so it did exactly that: on the doc's own
worked example the poll returned after 250ms of observed quiet. Sampling the
read's start instead makes the code the sentence.

The existing test measured from the end of the first read, which included an
interval plus a whole read, so it passed against the bug at any read length.
This commit is contained in:
pj committed 2026-08-22 21:17:11 +05:30
1 parent e713b0ea5e
commit d5f3937338
2 files changed
+12 -16

No files matched your search

@@ -159,15 +159,15 @@ internal fun pollUntilStable(
var streakStart = 0L var streakStart = 0L
while (System.currentTimeMillis() < deadline) { while (System.currentTimeMillis() < deadline) {
Thread.sleep(intervalMillis) Thread.sleep(intervalMillis)
val readStart = System.currentTimeMillis()
val current = try { val current = try {
snapshot() snapshot()
} catch (_: Exception) { } catch (_: Exception) {
null null
} }
val now = System.currentTimeMillis()
if (prior != null && current != null && prior == current) { if (prior != null && current != null && prior == current) {
if (streakStart == 0L) streakStart = now if (streakStart == 0L) streakStart = System.currentTimeMillis()
if (now - streakStart >= streakMillis) return if (readStart - streakStart >= streakMillis) return
} else { } else {
streakStart = 0L streakStart = 0L
} }
@@ -20,12 +20,9 @@ class StabilityPollTest {
} }
@Test fun slowSnapshotReadsDoNotEatTheStreak() { @Test fun slowSnapshotReadsDoNotEatTheStreak() {
// Every other test here uses an instant lambda and so passes whether or // A hierarchy fetch costs about this on a physical device, which is the
// not a read is charged to the streak; this one is the difference. // read pollUntilStable's contract works its numbers out against.
// StubDriverBackend's waitForIdle polls a real `uiautomator dump`, val readMillis = 500L
// which costs hundreds of milliseconds, so the slow read is the case it
// runs in.
val readMillis = 400L
val sampleStarts = mutableListOf<Long>() val sampleStarts = mutableListOf<Long>()
val sampleEnds = mutableListOf<Long>() val sampleEnds = mutableListOf<Long>()
val start = System.currentTimeMillis() val start = System.currentTimeMillis()
@@ -36,19 +33,18 @@ class StabilityPollTest {
"stable" "stable"
} }
// What the poll actually watched: the last read began this long after assertTrue(
// the first one returned, and every sample in between matched. sampleStarts.size >= 3,
val observedQuiet = sampleStarts.last() - sampleEnds.first() "a ${readMillis}ms read cannot clear the streak in one pair, starts=$sampleStarts",
)
val firstMatchingReadReturned = sampleEnds[1]
val observedQuiet = sampleStarts.last() - firstMatchingReadReturned
assertTrue( assertTrue(
observedQuiet >= MIN_STABLE_STREAK_MILLIS, observedQuiet >= MIN_STABLE_STREAK_MILLIS,
"the poll returned having observed only ${observedQuiet}ms of " + "the poll returned having observed only ${observedQuiet}ms of " +
"quiet, not ${MIN_STABLE_STREAK_MILLIS}ms; " + "quiet, not ${MIN_STABLE_STREAK_MILLIS}ms; " +
"starts=$sampleStarts ends=$sampleEnds", "starts=$sampleStarts ends=$sampleEnds",
) )
assertTrue(
sampleStarts.size >= 3,
"a ${readMillis}ms read cannot clear the streak in one pair, starts=$sampleStarts",
)
} }
@Test fun streakResetsOnAnyChange() { @Test fun streakResetsOnAnyChange() {