Skip to content

Commit 0ba2e4e

Browse files
slinznercopybara-androidxtest
authored andcommitted
Internal change.
PiperOrigin-RevId: 925485002
1 parent a3e3772 commit 0ba2e4e

7 files changed

Lines changed: 374 additions & 12 deletions

File tree

espresso/core/java/androidx/test/espresso/base/BUILD

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,11 @@ android_library(
6565
"IdleNotifier.java",
6666
"IdlingResourceRegistry.java",
6767
"Interrogator.java",
68+
"LegacyLookaheadDetector.java",
6869
"LooperIdlingResourceInterrogationHandler.java",
70+
"QueueIdleDetector.java",
6971
"TestLooperManagerCompat.java",
72+
"UnifiedRecentCountDetector.java",
7073
],
7174
deps = [
7275
"//espresso/core/java/androidx/test/espresso:interface",

espresso/core/java/androidx/test/espresso/base/Interrogator.java

Lines changed: 90 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,87 @@
2525
import android.os.SystemClock;
2626
import android.util.Log;
2727
import androidx.annotation.VisibleForTesting;
28+
import java.util.Locale;
2829

2930
/** Isolates the nasty details of touching the message queue. */
3031
final class Interrogator {
3132

3233
private static final String TAG = "Interrogator";
3334

3435
@VisibleForTesting static final int LOOKAHEAD_MILLIS = 15;
36+
37+
private static boolean useNewSync() {
38+
return Boolean.parseBoolean(System.getProperty("espresso.use_new_sync", "true"));
39+
}
40+
41+
private static boolean enableMlLogging() {
42+
return Boolean.parseBoolean(System.getProperty("espresso.enable_ml_logging", "true"));
43+
}
44+
45+
private static void logEvent(
46+
String event, long now, Long headWhen, boolean barrier, String decision) {
47+
if (!enableMlLogging()) {
48+
return;
49+
}
50+
String headWhenStr = headWhen == null ? "null" : String.valueOf(headWhen);
51+
Log.i(
52+
"ESPRESSO_ML",
53+
String.format(
54+
Locale.ROOT,
55+
"{\"event\":\"%s\",\"time\":%d,\"headWhen\":%s,\"barrier\":%b,\"decision\":\"%s\"}",
56+
event,
57+
now,
58+
headWhenStr,
59+
barrier,
60+
decision));
61+
}
62+
63+
private static void logDispatch(Message m) {
64+
if (!enableMlLogging()) {
65+
return;
66+
}
67+
long now = SystemClock.uptimeMillis();
68+
String target = m.getTarget() == null ? "null" : m.getTarget().getClass().getName();
69+
String callback = m.getCallback() == null ? "null" : m.getCallback().getClass().getName();
70+
Log.i(
71+
"ESPRESSO_ML",
72+
String.format(
73+
Locale.ROOT,
74+
"{\"event\":\"dispatch\",\"time\":%d,\"msgWhen\":%d,\"what\":%d,\"target\":\"%s\",\"callback\":\"%s\"}",
75+
now,
76+
m.getWhen(),
77+
m.what,
78+
target,
79+
callback));
80+
}
81+
82+
private static final ThreadLocal<QueueIdleDetector> detectorThreadLocal =
83+
new ThreadLocal<QueueIdleDetector>() {
84+
@Override
85+
protected QueueIdleDetector initialValue() {
86+
return createDefaultDetector();
87+
}
88+
};
89+
90+
private final QueueIdleDetector detector;
91+
92+
Interrogator() {
93+
this(detectorThreadLocal.get());
94+
}
95+
96+
@VisibleForTesting
97+
Interrogator(QueueIdleDetector detector) {
98+
this.detector = checkNotNull(detector);
99+
}
100+
101+
private static QueueIdleDetector createDefaultDetector() {
102+
if (useNewSync()) {
103+
return new UnifiedRecentCountDetector(50, 4);
104+
} else {
105+
return new LegacyLookaheadDetector(LOOKAHEAD_MILLIS);
106+
}
107+
}
108+
35109
private static final ThreadLocal<Boolean> interrogating =
36110
new ThreadLocal<Boolean>() {
37111
@Override
@@ -92,7 +166,6 @@ interface InterrogationHandler<R> extends QueueInterrogationHandler<R> {
92166
public String getMessage();
93167
}
94168

95-
Interrogator() {}
96169

97170
/**
98171
* Loops the main thread and informs the interrogation handler at interesting points in the exec
@@ -125,6 +198,8 @@ <T> T loopAndInterrogate(
125198
}
126199
stillInterested = handler.beforeTaskDispatch();
127200
handler.setMessage(m);
201+
logDispatch(m);
202+
detector.recordDispatch(SystemClock.uptimeMillis(), m);
128203
testLooperManager.execute(m);
129204

130205
// ensure looper invariants
@@ -181,27 +256,30 @@ <T> T peekAtQueueState(
181256
private boolean interrogateQueueState(
182257
TestLooperManagerCompat testLooperManager, QueueInterrogationHandler<?> handler) {
183258
synchronized (testLooperManager.getQueue()) {
259+
long now = SystemClock.uptimeMillis();
184260
if (testLooperManager.isBlockedOnSyncBarrier()) {
185261
if (Log.isLoggable(TAG, Log.DEBUG)) {
186262
Log.d(TAG, "barrier is up");
187263
}
264+
logEvent("interrogate", now, null, true, "barrier");
188265
return handler.barrierUp();
189266
}
190267
Long headWhen = testLooperManager.peekWhen();
191-
if (headWhen == null) {
192-
return handler.queueEmpty();
193-
}
194268

195-
long nowFuz = SystemClock.uptimeMillis() + LOOKAHEAD_MILLIS;
196-
if (Log.isLoggable(TAG, Log.DEBUG)) {
197-
Log.d(
198-
TAG,
199-
"headWhen: " + headWhen + " nowFuz: " + nowFuz + " due long: " + (nowFuz < headWhen));
200-
}
201-
if (nowFuz > headWhen) {
269+
boolean isUnifiedIdle = detector.isIdle(now, headWhen);
270+
271+
if (isUnifiedIdle) {
272+
if (headWhen == null) {
273+
logEvent("interrogate", now, null, false, "empty");
274+
return handler.queueEmpty();
275+
} else {
276+
logEvent("interrogate", now, headWhen, false, "long");
277+
return handler.taskDueLong();
278+
}
279+
} else {
280+
logEvent("interrogate", now, headWhen, false, "soon");
202281
return handler.taskDueSoon();
203282
}
204-
return handler.taskDueLong();
205283
}
206284
}
207285

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* Copyright (C) 2026 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package androidx.test.espresso.base;
18+
19+
import android.os.Message;
20+
21+
/** Legacy lookahead-based idle detector. */
22+
final class LegacyLookaheadDetector implements QueueIdleDetector {
23+
24+
private final int lookaheadMillis;
25+
26+
LegacyLookaheadDetector(int lookaheadMillis) {
27+
this.lookaheadMillis = lookaheadMillis;
28+
}
29+
30+
@Override
31+
public void recordDispatch(long now, Message m) {
32+
// No-op. Legacy detector does not track dispatch history.
33+
}
34+
35+
@Override
36+
public boolean isIdle(long now, Long headWhen) {
37+
if (headWhen == null) {
38+
return true;
39+
}
40+
long nowFuz = now + lookaheadMillis;
41+
return nowFuz <= headWhen;
42+
}
43+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*
2+
* Copyright (C) 2026 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package androidx.test.espresso.base;
18+
19+
import android.os.Message;
20+
21+
/** Interface for deciding if a MessageQueue under interrogation is idle. */
22+
interface QueueIdleDetector {
23+
24+
/** Records that a message has been dispatched at the given time. */
25+
void recordDispatch(long now, Message m);
26+
27+
/**
28+
* Evaluates whether the queue is idle.
29+
*
30+
* @param now the current uptime millis
31+
* @param headWhen the execution uptime millis of the next message in the queue, or null if empty
32+
* @return true if the queue should be considered idle, false otherwise.
33+
*/
34+
boolean isIdle(long now, Long headWhen);
35+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/*
2+
* Copyright (C) 2026 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package androidx.test.espresso.base;
18+
19+
import android.os.Message;
20+
import java.util.ArrayDeque;
21+
22+
/** Unified Recent Count heuristic detector (T=50ms, C=4). */
23+
final class UnifiedRecentCountDetector implements QueueIdleDetector {
24+
25+
private final long windowMs;
26+
private final int maxDispatches;
27+
private final ArrayDeque<Long> dispatchHistory = new ArrayDeque<>();
28+
29+
UnifiedRecentCountDetector(long windowMs, int maxDispatches) {
30+
this.windowMs = windowMs;
31+
this.maxDispatches = maxDispatches;
32+
}
33+
34+
@Override
35+
public synchronized void recordDispatch(long now, Message m) {
36+
dispatchHistory.addLast(now);
37+
pruneHistory(now);
38+
}
39+
40+
@Override
41+
public synchronized boolean isIdle(long now, Long headWhen) {
42+
pruneHistory(now);
43+
long recentDispatches = dispatchHistory.size();
44+
45+
boolean isIdleHeuristic = false;
46+
if (headWhen == null) {
47+
isIdleHeuristic = true;
48+
} else {
49+
long headDelay = headWhen - now;
50+
if (headDelay > windowMs) {
51+
isIdleHeuristic = true;
52+
}
53+
}
54+
55+
return isIdleHeuristic && recentDispatches <= maxDispatches;
56+
}
57+
58+
private void pruneHistory(long now) {
59+
long threshold = now - windowMs;
60+
while (!dispatchHistory.isEmpty() && dispatchHistory.peekFirst() < threshold) {
61+
dispatchHistory.removeFirst();
62+
}
63+
}
64+
}

espresso/core/javatests/androidx/test/espresso/base/BUILD

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,3 +302,15 @@ axt_android_library_test(
302302
"@maven//:junit_junit",
303303
],
304304
)
305+
306+
axt_android_library_test(
307+
name = "QueueIdleDetectorTest",
308+
srcs = ["QueueIdleDetectorTest.java"],
309+
deps = [
310+
"//core",
311+
"//espresso/core/java/androidx/test/espresso/base:idling_resource_registry",
312+
"//ext/junit",
313+
"@maven//:com_google_truth_truth",
314+
"@maven//:junit_junit",
315+
],
316+
)

0 commit comments

Comments
 (0)