Skip to content

Commit 9b45990

Browse files
authored
fix(timers): order timers with the Java MessageQueue instead of ALooper fds (#1967)
* feat(inspector): serve source maps to DevTools via Network.loadNetworkResource Chrome DevTools no longer fetches external source maps itself when debugging remote targets: it issues Network.loadNetworkResource to the target and reads the result back through IO.read/IO.close. None of these embedder-side CDP domains are implemented by V8's inspector, so external source maps failed and apps had to fall back to bloated inline-source-map builds. - Handle Network.loadNetworkResource natively: resolve the URL back to a file on disk and reply with a stream handle (success:false + net::ERR_FILE_NOT_FOUND when missing). - Implement IO.read (1MB base64 chunks; eof only on a final empty read, since the frontend discards data accompanying eof) and IO.close. - Reply with a JSON-RPC error for unsupported schemes (e.g. https) so DevTools keeps its existing fallback of fetching from the host. - Rewrite sourceMapURL in outgoing Debugger.scriptParsed / Debugger.scriptFailedToParse events from relative/file:// URLs to a custom nsruntime:// scheme. DevTools hard-excludes file:, data: and devtools: URLs from loading through the target, so without the rewrite it would never send Network.loadNetworkResource and instead try (and fail) to read device files from the host machine. data: and http(s) URLs are left untouched, keeping inline source maps working. - Allow opting out via nativescript.config.ts: android.disableSourceMapURLRewrite (or the same key at the top level). - Serve these messages on the websocket read thread (new native handleMessageOnSocketThread), since the main-thread queue is unavailable exactly when DevTools needs source maps: the pause loop bypasses dispatchMessage and a busy isolate never drains the queue. The handler is V8-free and returns the response for Java to send on the receiving socket. - Make Debugger.pause interrupt busy JS via Isolate::RequestInterrupt, skipped while already paused in the nested loop to avoid a spurious re-pause after resume. - Vendor nlohmann/json v3.12.0 (third_party/json.hpp, header-only) for CDP message handling outside V8. Ports NativeScript/ios#385 and NativeScript/ios#378 to Android. Refs: nodejs/node#58077 * fix: timers removed from wrong looper * 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 9b45990

9 files changed

Lines changed: 26384 additions & 156 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;

test-app/app/src/main/java/com/tns/AndroidJsV8Inspector.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ class AndroidJsV8Inspector {
3737

3838
protected native final void dispatchMessage(String message);
3939

40+
private native String handleMessageOnSocketThread(String message);
41+
4042
private Handler mainHandler;
4143

4244
private final Object debugBrkLock;
@@ -294,6 +296,22 @@ protected void onMessage(final NanoWSD.WebSocketFrame message) {
294296
Log.d("V8Inspector", "To dbg backend: " + message.getTextPayload() + " ThreadId:" + Thread.currentThread().getId());
295297
}
296298

299+
// Network.loadNetworkResource / IO.read / IO.close are served from
300+
// disk on this thread so source maps load even while the isolate is
301+
// paused at a breakpoint or busy running JS; Debugger.pause schedules
302+
// a V8 interrupt and still flows through the queue.
303+
String fastPathResponse = handleMessageOnSocketThread(message.getTextPayload());
304+
if (fastPathResponse != null) {
305+
try {
306+
send(fastPathResponse);
307+
} catch (IOException e) {
308+
if (com.tns.Runtime.isDebuggable()) {
309+
e.printStackTrace();
310+
}
311+
}
312+
return;
313+
}
314+
297315
inspectorMessages.offer(message.getTextPayload());
298316

299317
if (!AndroidJsV8Inspector.ReadyToProcessMessages.get()) {

0 commit comments

Comments
 (0)