Skip to content

Commit bfd7650

Browse files
committed
fix(timers): order timers with the Java MessageQueue instead of ALooper fds
Timers were delivered through a pipe fd registered with ALooper_addFd. Android services fd callbacks inside MessageQueue.nativePollOnce at every message boundary, so timer callbacks and Handler messages lived in two queues with no mutual ordering: a setTimeout(0) could fire before or interleave around an already-queued Handler.postDelayed(0) runnable. Timers now ride the Java MessageQueue itself via a dedicated per-runtime TimerHandler bound to the isolate's Looper: - Each scheduled timer enqueues one message with sendMessageAtTime, so timers share a single queue with Handler.post/postDelayed and fire in exact MessageQueue order. - Messages are anonymous "due tokens": a native list sorted by exact (sub-millisecond) due time picks the earliest due timer per token, preserving the previous relative ordering of JS timers despite the millisecond-quantized Java queue. - Due-now timers post at (long)now so they tie (FIFO) with a postDelayed(0) made in the same millisecond; future timers post at ceil(dueTime) so they never fire early. - The background watcher thread, pipe, mutex and condition variables are removed; the MessageQueue does all delayed scheduling and everything runs on the isolate's thread with no locking. Worker isolates get the same behavior on their own loopers. Also adds ordering regression tests (timers vs Handler posts), scheduled from a java-posted runnable to avoid the >=5-level nesting clamp that jasmine's spec chaining otherwise triggers. Fixes timer/Handler ordering so setTimeout(0) reliably yields behind already-queued main-thread work.
1 parent 3a2ad87 commit bfd7650

4 files changed

Lines changed: 265 additions & 155 deletions

File tree

test-app/app/src/main/assets/app/tests/testNativeTimers.js

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,49 @@ describe('native timer', () => {
9494
done();
9595
});
9696
});
97+
// these specs schedule from a java-posted runnable so they run outside any
98+
// timer callback: jasmine chains specs through timer callbacks, and when
99+
// the runtime is built with NS_TIMERS_NESTING_CLAMP the nesting clamp
100+
// (>=5 levels -> 4ms minimum) would otherwise make setTimeout(0)
101+
// legitimately lose to a postDelayed(0)
102+
it('preserves order with java handler posts', (done) => {
103+
const order = [];
104+
const handler = new android.os.Handler(android.os.Looper.myLooper());
105+
handler.post(new java.lang.Runnable({
106+
run: () => {
107+
setTimeout(() => order.push(1));
108+
handler.postDelayed(new java.lang.Runnable({ run: () => order.push(2) }), 0);
109+
setTimeout(() => order.push(3));
110+
setTimeout(() => {
111+
expect(order.join(',')).toBe('1,2,3');
112+
done();
113+
}, 100);
114+
}
115+
}));
116+
});
117+
118+
it('interleaves many timers with a java handler post', (done) => {
119+
const order = [];
120+
const handler = new android.os.Handler(android.os.Looper.myLooper());
121+
handler.post(new java.lang.Runnable({
122+
run: () => {
123+
for (let i = 0; i < 50; i++) {
124+
setTimeout(() => order.push('t'));
125+
}
126+
handler.postDelayed(new java.lang.Runnable({ run: () => order.push('j') }), 0);
127+
for (let i = 0; i < 50; i++) {
128+
setTimeout(() => order.push('t'));
129+
}
130+
setTimeout(() => {
131+
// the java runnable must land exactly between the two timer batches
132+
expect(order.indexOf('j')).toBe(50);
133+
expect(order.length).toBe(101);
134+
done();
135+
}, 100);
136+
}
137+
}));
138+
});
139+
97140
it('frees up resources after complete', (done) => {
98141
let timeout = 0;
99142
let interval = 0;

0 commit comments

Comments
 (0)