From d6ca8b592c1548463e3635c6cc70645c3373a907 Mon Sep 17 00:00:00 2001 From: Eduardo Speroni Date: Wed, 11 Feb 2026 16:30:24 -0300 Subject: [PATCH 1/5] fix: multithreadedJS should use concurrent java maps --- .../src/main/java/com/tns/Runtime.java | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/test-app/runtime/src/main/java/com/tns/Runtime.java b/test-app/runtime/src/main/java/com/tns/Runtime.java index 14c2aa438..99d6bc8ce 100644 --- a/test-app/runtime/src/main/java/com/tns/Runtime.java +++ b/test-app/runtime/src/main/java/com/tns/Runtime.java @@ -35,6 +35,7 @@ import java.util.concurrent.FutureTask; import java.util.concurrent.RunnableFuture; import java.util.concurrent.atomic.AtomicInteger; +import java.util.Collections; public class Runtime { private native void initNativeScript(int runtimeId, String filesPath, String nativeLibDir, boolean verboseLoggingEnabled, boolean isDebuggable, String packageName, @@ -103,15 +104,15 @@ public static void passSuppressedExceptionToJs(Throwable ex, String methodName) "Primitive types need to be manually wrapped in their respective Object wrappers.\n" + "If you are creating an instance of an inner class, make sure to always provide reference to the outer `this` as the first argument."; - private HashMap strongInstances = new HashMap<>(); + private Map strongInstances = new HashMap<>(); - private HashMap> weakInstances = new HashMap<>(); + private Map> weakInstances = new HashMap<>(); - private NativeScriptHashMap strongJavaObjectToID = new NativeScriptHashMap(); + private Map strongJavaObjectToID = new NativeScriptHashMap(); - private NativeScriptWeakHashMap weakJavaObjectToID = new NativeScriptWeakHashMap(); + private Map weakJavaObjectToID = new NativeScriptWeakHashMap(); - private final Map, JavaScriptImplementation> loadedJavaScriptExtends = new HashMap, JavaScriptImplementation>(); + private Map, JavaScriptImplementation> loadedJavaScriptExtends = new HashMap, JavaScriptImplementation>(); private final java.lang.Runtime dalvikRuntime = java.lang.Runtime.getRuntime(); @@ -215,6 +216,14 @@ public Runtime(StaticConfiguration config, DynamicConfiguration dynamicConfigura if (dynamicConfiguration.mainThreadScheduler != null) { this.mainThreadHandler = dynamicConfiguration.mainThreadScheduler.getHandler(); } + // if multithreadedJS, make all maps concurrent or synchronized: + if (config.appConfig.getEnableMultithreadedJavascript()) { + this.strongInstances = new ConcurrentHashMap<>(); + this.weakInstances = new ConcurrentHashMap<>(); + this.loadedJavaScriptExtends = new ConcurrentHashMap, JavaScriptImplementation>(); + this.strongJavaObjectToID = Collections.synchronizedMap(new NativeScriptHashMap()); + this.weakJavaObjectToID = Collections.synchronizedMap(new NativeScriptWeakHashMap()); + } classResolver = new ClassResolver(classStorageService); currentRuntime.set(this); From d6e2afeb517aae073a0bf7851cb48f9446e97495 Mon Sep 17 00:00:00 2001 From: Eduardo Speroni Date: Wed, 11 Feb 2026 18:48:32 -0300 Subject: [PATCH 2/5] test: add regression test for high contention calls --- test-app/app/src/main/assets/app/mainpage.js | 1 + .../assets/app/tests/testConcurrentAccess.js | 78 +++++++++++++++++++ .../com/tns/tests/ConcurrentAccessTest.java | 76 ++++++++++++++++++ 3 files changed, 155 insertions(+) create mode 100644 test-app/app/src/main/assets/app/tests/testConcurrentAccess.js create mode 100644 test-app/app/src/main/java/com/tns/tests/ConcurrentAccessTest.java diff --git a/test-app/app/src/main/assets/app/mainpage.js b/test-app/app/src/main/assets/app/mainpage.js index c3fd1f3d8..0a8c79b00 100644 --- a/test-app/app/src/main/assets/app/mainpage.js +++ b/test-app/app/src/main/assets/app/mainpage.js @@ -72,5 +72,6 @@ require('./tests/testURLImpl.js'); require('./tests/testURLSearchParamsImpl.js'); require('./tests/testPerformanceNow'); require('./tests/testQueueMicrotask'); +require("./tests/testConcurrentAccess"); require("./tests/testESModules.mjs"); diff --git a/test-app/app/src/main/assets/app/tests/testConcurrentAccess.js b/test-app/app/src/main/assets/app/tests/testConcurrentAccess.js new file mode 100644 index 000000000..c8ac32c10 --- /dev/null +++ b/test-app/app/src/main/assets/app/tests/testConcurrentAccess.js @@ -0,0 +1,78 @@ +// WARNING: IF THIS TEST FAILS IT COMPLETELY BREAKS ALL OTHER TESTS! + +describe("Tests concurrent access to JNI", function () { + // Customizable test parameters + const BACKGROUND_THREADS = 5; + const SYNC_CALLS = 2; + const ITERATIONS_PER_CALL = 100; + const TIMEOUT_MS = 3000; + + it("test_high_contention_concurrent_access_with_multiple_objects", (done) => { + console.log('STARTING PROBLEMATIC TEST. THIS MIGHT CRASH OR CAUSE ISSUES IN OTHER TESTS IF IT FAILS. If this is close to the end of the log, check test_high_contention_concurrent_access_with_multiple_objects'); + let callbackInvocations = 0; + + const callback = new com.tns.tests.ConcurrentAccessTest.Callback({ + invoke: ( + list1, + list2, + list3, + list4, + list5, + list6, + list7, + list8, + list9, + list10, + ) => { + callbackInvocations++; + // Assert that accessing size() on any of the lists doesn't throw + expect(() => list1.size()).not.toThrow(); + expect(() => list2.size()).not.toThrow(); + expect(() => list3.size()).not.toThrow(); + expect(() => list4.size()).not.toThrow(); + expect(() => list5.size()).not.toThrow(); + expect(() => list6.size()).not.toThrow(); + expect(() => list7.size()).not.toThrow(); + expect(() => list8.size()).not.toThrow(); + expect(() => list9.size()).not.toThrow(); + expect(() => list10.size()).not.toThrow(); + + // Verify that the lists actually have content + expect(list1.size()).toBe(5); + expect(list2.size()).toBe(5); + expect(list3.size()).toBe(5); + expect(list4.size()).toBe(5); + expect(list5.size()).toBe(5); + expect(list6.size()).toBe(5); + expect(list7.size()).toBe(5); + expect(list8.size()).toBe(5); + expect(list9.size()).toBe(5); + expect(list10.size()).toBe(5); + }, + }); + + // Start multiple background threads + for (let i = 0; i < BACKGROUND_THREADS; i++) { + com.tns.tests.ConcurrentAccessTest.callFromBackgroundThread( + callback, + ITERATIONS_PER_CALL, + ); + } + + // Call synchronously multiple times + for (let i = 0; i < SYNC_CALLS; i++) { + com.tns.tests.ConcurrentAccessTest.callSynchronously( + callback, + ITERATIONS_PER_CALL, + ); + } + + // Wait for all threads to complete + setTimeout(() => { + const expectedInvocations = + (BACKGROUND_THREADS + SYNC_CALLS) * ITERATIONS_PER_CALL; + expect(callbackInvocations).toBe(expectedInvocations); + done(); + }, TIMEOUT_MS); + }); +}); diff --git a/test-app/app/src/main/java/com/tns/tests/ConcurrentAccessTest.java b/test-app/app/src/main/java/com/tns/tests/ConcurrentAccessTest.java new file mode 100644 index 000000000..acd9d8538 --- /dev/null +++ b/test-app/app/src/main/java/com/tns/tests/ConcurrentAccessTest.java @@ -0,0 +1,76 @@ +package com.tns.tests; + +import java.util.ArrayList; + +public class ConcurrentAccessTest { + + public interface Callback { + void invoke(ArrayList list1, ArrayList list2, ArrayList list3, ArrayList list4, ArrayList list5, + ArrayList list6, ArrayList list7, ArrayList list8, ArrayList list9, ArrayList list10); + } + + public interface ErrorCallback { + void onError(Throwable error); + } + + /** + * Calls the callback from a background thread multiple times. + * @param callback The callback to invoke + * @param times Number of times to call the callback (default 50) + */ + public static void callFromBackgroundThread(final Callback callback, final int times) { + Thread thread = new Thread(new Runnable() { + @Override + public void run() { + for (int i = 0; i < times; i++) { + invokeCallbackWithArrayLists(callback, i); + } + } + }); + thread.start(); + } + + /** + * Calls the callback synchronously from the current thread. + * @param callback The callback to invoke + * @param times Number of times to call the callback (default 50) + */ + public static void callSynchronously(Callback callback, int times) { + for (int i = 0; i < times; i++) { + invokeCallbackWithArrayLists(callback, i); + } + } + + /** + * Helper method that creates 10 ArrayLists and invokes the callback with them. + * Each ArrayList contains some data based on the iteration number. + */ + private static void invokeCallbackWithArrayLists(Callback callback, int iteration) { + ArrayList list1 = new ArrayList<>(); + ArrayList list2 = new ArrayList<>(); + ArrayList list3 = new ArrayList<>(); + ArrayList list4 = new ArrayList<>(); + ArrayList list5 = new ArrayList<>(); + ArrayList list6 = new ArrayList<>(); + ArrayList list7 = new ArrayList<>(); + ArrayList list8 = new ArrayList<>(); + ArrayList list9 = new ArrayList<>(); + ArrayList list10 = new ArrayList<>(); + + // Add some data to each list + for (int i = 0; i < 5; i++) { + list1.add(iteration * 10 + i); + list2.add(iteration * 10 + i + 1); + list3.add(iteration * 10 + i + 2); + list4.add(iteration * 10 + i + 3); + list5.add(iteration * 10 + i + 4); + list6.add(iteration * 10 + i + 5); + list7.add(iteration * 10 + i + 6); + list8.add(iteration * 10 + i + 7); + list9.add(iteration * 10 + i + 8); + list10.add(iteration * 10 + i + 9); + } + + callback.invoke(list1, list2, list3, list4, list5, list6, list7, list8, list9, list10); + } +} \ No newline at end of file From 2a694fbef4a6aec349f303c95604bba2aad776e2 Mon Sep 17 00:00:00 2001 From: Eduardo Speroni Date: Wed, 11 Feb 2026 19:14:06 -0300 Subject: [PATCH 3/5] fix: revert to synchronized maps for loadedJavaScriptExtends to resolve InvocationTargetException --- test-app/runtime/src/main/java/com/tns/Runtime.java | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/test-app/runtime/src/main/java/com/tns/Runtime.java b/test-app/runtime/src/main/java/com/tns/Runtime.java index 99d6bc8ce..0b164776e 100644 --- a/test-app/runtime/src/main/java/com/tns/Runtime.java +++ b/test-app/runtime/src/main/java/com/tns/Runtime.java @@ -220,9 +220,13 @@ public Runtime(StaticConfiguration config, DynamicConfiguration dynamicConfigura if (config.appConfig.getEnableMultithreadedJavascript()) { this.strongInstances = new ConcurrentHashMap<>(); this.weakInstances = new ConcurrentHashMap<>(); - this.loadedJavaScriptExtends = new ConcurrentHashMap, JavaScriptImplementation>(); - this.strongJavaObjectToID = Collections.synchronizedMap(new NativeScriptHashMap()); - this.weakJavaObjectToID = Collections.synchronizedMap(new NativeScriptWeakHashMap()); + // TODO: figure out why using a concurrent map for loadedJavaScriptExtends + // results in a fail of TestCanFindImplementationObjectWhenCreateExtendedObjectFromJava + // with java.lang.reflect.InvocationTargetException + // but works with synchronizeMap + this.loadedJavaScriptExtends = Collections.synchronizedMap(new HashMap<>()); + this.strongJavaObjectToID = Collections.synchronizedMap(new NativeScriptHashMap<>()); + this.weakJavaObjectToID = Collections.synchronizedMap(new NativeScriptWeakHashMap<>()); } classResolver = new ClassResolver(classStorageService); From e0e92bf67f34d3fc368269346c23dda5b9cbaa9c Mon Sep 17 00:00:00 2001 From: Eduardo Speroni Date: Wed, 11 Feb 2026 19:25:57 -0300 Subject: [PATCH 4/5] chore: update readme --- test-app/runtime/src/main/java/com/tns/Runtime.java | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/test-app/runtime/src/main/java/com/tns/Runtime.java b/test-app/runtime/src/main/java/com/tns/Runtime.java index 0b164776e..ae1b50ec8 100644 --- a/test-app/runtime/src/main/java/com/tns/Runtime.java +++ b/test-app/runtime/src/main/java/com/tns/Runtime.java @@ -220,11 +220,9 @@ public Runtime(StaticConfiguration config, DynamicConfiguration dynamicConfigura if (config.appConfig.getEnableMultithreadedJavascript()) { this.strongInstances = new ConcurrentHashMap<>(); this.weakInstances = new ConcurrentHashMap<>(); - // TODO: figure out why using a concurrent map for loadedJavaScriptExtends - // results in a fail of TestCanFindImplementationObjectWhenCreateExtendedObjectFromJava - // with java.lang.reflect.InvocationTargetException - // but works with synchronizeMap - this.loadedJavaScriptExtends = Collections.synchronizedMap(new HashMap<>()); + // TODO: can't use a ConcurrentHashMap for loadedJavaScriptExtends because it loads null objects, which aren't supported + // either leave it like this or create a separate set for null caches + this.loadedJavaScriptExtends = new ConcurrentHashMap<>(); this.strongJavaObjectToID = Collections.synchronizedMap(new NativeScriptHashMap<>()); this.weakJavaObjectToID = Collections.synchronizedMap(new NativeScriptWeakHashMap<>()); } From 17aa67862e666d6e1c7bedc9cc5eb7ce32524e1f Mon Sep 17 00:00:00 2001 From: Eduardo Speroni Date: Wed, 11 Feb 2026 20:25:23 -0300 Subject: [PATCH 5/5] fix: switch loadedJavaScriptExtends to synchronized map to prevent null object issues --- test-app/runtime/src/main/java/com/tns/Runtime.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-app/runtime/src/main/java/com/tns/Runtime.java b/test-app/runtime/src/main/java/com/tns/Runtime.java index ae1b50ec8..57f88f9f3 100644 --- a/test-app/runtime/src/main/java/com/tns/Runtime.java +++ b/test-app/runtime/src/main/java/com/tns/Runtime.java @@ -222,7 +222,7 @@ public Runtime(StaticConfiguration config, DynamicConfiguration dynamicConfigura this.weakInstances = new ConcurrentHashMap<>(); // TODO: can't use a ConcurrentHashMap for loadedJavaScriptExtends because it loads null objects, which aren't supported // either leave it like this or create a separate set for null caches - this.loadedJavaScriptExtends = new ConcurrentHashMap<>(); + this.loadedJavaScriptExtends = Collections.synchronizedMap(new HashMap<>()); this.strongJavaObjectToID = Collections.synchronizedMap(new NativeScriptHashMap<>()); this.weakJavaObjectToID = Collections.synchronizedMap(new NativeScriptWeakHashMap<>()); }