after() busy-spins for its whole duration once verification succeeds
What happens
after(millis) is documented to wait the full period and then report the result. Internally,
VerificationOverTimeImpl.verify() polls the delegate mode in a loop until the timer runs out:
while (timer.isCounting()) {
try {
delegate.verify(data);
if (returnOnSuccess) {
return;
} else {
error = null;
}
} catch (AssertionError e) {
error = handleVerifyException(e); // sleeps pollingPeriodMillis
}
}The only sleep() is inside handleVerifyException, i.e. on the failure path. For after(),
returnOnSuccess is false, so once the delegate is satisfied the loop re-runs the full
verification back to back with no pause until the duration elapses.
After configures a 10 ms polling period, so after(200) is meant to poll about 20 times.
In practice it polls millions of times and keeps one core at 100% for the whole window.
Evidence
Before #3859, every poll re-captured each matching invocation once, so the number of captured values is a direct count of how many times the loop ran:
- The standalone reproduction of the loop in #3859 captured ~13.8 million values for
after(200).times(3)with three invocations — about 4.6 million polls in 200 ms. - @raphw reproduced the over-capture against real Mockito while reviewing #3859 (~27k values with the fix reverted). Real Mockito does much more work per poll, so the count is lower, but it is still far above the ~20 polls a 10 ms period implies for a 200 ms window.
#3859 stops the memory growth by capturing each invocation once, but deliberately leaves the loop
alone, so the CPU cost is unchanged: every after() in a test suite pins a core for its whole
duration, and each pass re-scans every invocation on the mock.
Expected
Once the delegate is satisfied, after() should wait pollingPeriodMillis before polling again,
the same as it does after a failed poll. The result for after() can only change when new
invocations arrive, so polling every 10 ms loses nothing.
Possible fix
Sleep on the success path when returnOnSuccess is false, e.g.
if (returnOnSuccess) {
return;
}
error = null;
sleep(pollingPeriodMillis);timeout() is unaffected (it returns on the first success). The early-failure behaviour for
atMost() / never() is unaffected (that path throws immediately in handleVerifyException).
This touches the same method as #3859, so I'd suggest landing it after that PR. Happy to send the
PR once #3859 is settled, with a test that bounds the number of delegate polls during an
after() window.
Source: mockito/mockito