Summary
Compose UI tests that render lists via Jetpack Paging (collectAsLazyPagingItems()) flake on CI shard jobs with content not displayed / ComposeTimeoutException, deterministically once enough test classes share a single test fork. They pass on dev machines. MessagingScreenTest is the current victim (26 of its paged-content tests), surfaced when an added test class reshuffled the index % 4 shard partition and moved it into a busier shard.
Root cause
The kotlinx Dispatchers.Default pool is sized to the CPU count — ~2 on GitHub-hosted runners. Many unit tests start background coroutines (managers/pollers with delay() loops, etc.) and don't cancel them in teardown. These accumulate within a fork (capped by the existing setForkEvery(10)) and create enough scheduler churn on a 2-thread pool that Paging's AsyncPagingDataDiffer worker can't get serviced, so items never present and the assertions see an empty list.
Diagnosis (reproduced locally)
Shrinking the Default pool to CI size reproduces CI exactly:
-Dkotlinx.coroutines.scheduler.core.pool.size: full shard fails at pool ≤ 8, passes at pool = 16.
- Thread dumps show Default workers parked (idle), not blocked — i.e. diffuse suspended-coroutine churn from many tests, not one blocking leaker. No single class/quarter reproduces; only the aggregate does (8+ contributors).
Mitigation applied
PR #906 pins a fixed Default-pool floor for unit tests in app/build.gradle.kts (independent of host core count), complementing the existing forkEvery(10). This unblocks CI but masks the underlying leaks.
Cleanup owed (this issue)
Audit unit tests that start background coroutines (CoroutineScope(Dispatchers.Default/IO), managers, pollers, collectors) and ensure they cancel/stop in @After. Once the leaks are gone, the pool floor can be lowered or removed. Detection harness: run any Compose+Paging test preceded by suspects under -Dkotlinx.coroutines.scheduler.core.pool.size=1 -D...max.pool.size=2 and watch for the timeout.
— filed by claude-opus-4-8
Summary
Compose UI tests that render lists via Jetpack Paging (
collectAsLazyPagingItems()) flake on CI shard jobs withcontent not displayed/ComposeTimeoutException, deterministically once enough test classes share a single test fork. They pass on dev machines.MessagingScreenTestis the current victim (26 of its paged-content tests), surfaced when an added test class reshuffled theindex % 4shard partition and moved it into a busier shard.Root cause
The kotlinx
Dispatchers.Defaultpool is sized to the CPU count — ~2 on GitHub-hosted runners. Many unit tests start background coroutines (managers/pollers withdelay()loops, etc.) and don't cancel them in teardown. These accumulate within a fork (capped by the existingsetForkEvery(10)) and create enough scheduler churn on a 2-thread pool that Paging'sAsyncPagingDataDifferworker can't get serviced, so items never present and the assertions see an empty list.Diagnosis (reproduced locally)
Shrinking the Default pool to CI size reproduces CI exactly:
-Dkotlinx.coroutines.scheduler.core.pool.size: full shard fails at pool ≤ 8, passes at pool = 16.Mitigation applied
PR #906 pins a fixed Default-pool floor for unit tests in
app/build.gradle.kts(independent of host core count), complementing the existingforkEvery(10). This unblocks CI but masks the underlying leaks.Cleanup owed (this issue)
Audit unit tests that start background coroutines (
CoroutineScope(Dispatchers.Default/IO), managers, pollers, collectors) and ensure they cancel/stop in@After. Once the leaks are gone, the pool floor can be lowered or removed. Detection harness: run any Compose+Paging test preceded by suspects under-Dkotlinx.coroutines.scheduler.core.pool.size=1 -D...max.pool.size=2and watch for the timeout.— filed by claude-opus-4-8