Skip to content

Commit 1fd144f

Browse files
authored
fix: multithreadedJS should use concurrent java maps (#1920)
1 parent e924542 commit 1fd144f

File tree

4 files changed

+171
-5
lines changed

4 files changed

+171
-5
lines changed

test-app/app/src/main/assets/app/mainpage.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,5 +72,6 @@ require('./tests/testURLImpl.js');
7272
require('./tests/testURLSearchParamsImpl.js');
7373
require('./tests/testPerformanceNow');
7474
require('./tests/testQueueMicrotask');
75+
require("./tests/testConcurrentAccess");
7576

7677
require("./tests/testESModules.mjs");
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
// WARNING: IF THIS TEST FAILS IT COMPLETELY BREAKS ALL OTHER TESTS!
2+
3+
describe("Tests concurrent access to JNI", function () {
4+
// Customizable test parameters
5+
const BACKGROUND_THREADS = 5;
6+
const SYNC_CALLS = 2;
7+
const ITERATIONS_PER_CALL = 100;
8+
const TIMEOUT_MS = 3000;
9+
10+
it("test_high_contention_concurrent_access_with_multiple_objects", (done) => {
11+
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');
12+
let callbackInvocations = 0;
13+
14+
const callback = new com.tns.tests.ConcurrentAccessTest.Callback({
15+
invoke: (
16+
list1,
17+
list2,
18+
list3,
19+
list4,
20+
list5,
21+
list6,
22+
list7,
23+
list8,
24+
list9,
25+
list10,
26+
) => {
27+
callbackInvocations++;
28+
// Assert that accessing size() on any of the lists doesn't throw
29+
expect(() => list1.size()).not.toThrow();
30+
expect(() => list2.size()).not.toThrow();
31+
expect(() => list3.size()).not.toThrow();
32+
expect(() => list4.size()).not.toThrow();
33+
expect(() => list5.size()).not.toThrow();
34+
expect(() => list6.size()).not.toThrow();
35+
expect(() => list7.size()).not.toThrow();
36+
expect(() => list8.size()).not.toThrow();
37+
expect(() => list9.size()).not.toThrow();
38+
expect(() => list10.size()).not.toThrow();
39+
40+
// Verify that the lists actually have content
41+
expect(list1.size()).toBe(5);
42+
expect(list2.size()).toBe(5);
43+
expect(list3.size()).toBe(5);
44+
expect(list4.size()).toBe(5);
45+
expect(list5.size()).toBe(5);
46+
expect(list6.size()).toBe(5);
47+
expect(list7.size()).toBe(5);
48+
expect(list8.size()).toBe(5);
49+
expect(list9.size()).toBe(5);
50+
expect(list10.size()).toBe(5);
51+
},
52+
});
53+
54+
// Start multiple background threads
55+
for (let i = 0; i < BACKGROUND_THREADS; i++) {
56+
com.tns.tests.ConcurrentAccessTest.callFromBackgroundThread(
57+
callback,
58+
ITERATIONS_PER_CALL,
59+
);
60+
}
61+
62+
// Call synchronously multiple times
63+
for (let i = 0; i < SYNC_CALLS; i++) {
64+
com.tns.tests.ConcurrentAccessTest.callSynchronously(
65+
callback,
66+
ITERATIONS_PER_CALL,
67+
);
68+
}
69+
70+
// Wait for all threads to complete
71+
setTimeout(() => {
72+
const expectedInvocations =
73+
(BACKGROUND_THREADS + SYNC_CALLS) * ITERATIONS_PER_CALL;
74+
expect(callbackInvocations).toBe(expectedInvocations);
75+
done();
76+
}, TIMEOUT_MS);
77+
});
78+
});
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package com.tns.tests;
2+
3+
import java.util.ArrayList;
4+
5+
public class ConcurrentAccessTest {
6+
7+
public interface Callback {
8+
void invoke(ArrayList list1, ArrayList list2, ArrayList list3, ArrayList list4, ArrayList list5,
9+
ArrayList list6, ArrayList list7, ArrayList list8, ArrayList list9, ArrayList list10);
10+
}
11+
12+
public interface ErrorCallback {
13+
void onError(Throwable error);
14+
}
15+
16+
/**
17+
* Calls the callback from a background thread multiple times.
18+
* @param callback The callback to invoke
19+
* @param times Number of times to call the callback (default 50)
20+
*/
21+
public static void callFromBackgroundThread(final Callback callback, final int times) {
22+
Thread thread = new Thread(new Runnable() {
23+
@Override
24+
public void run() {
25+
for (int i = 0; i < times; i++) {
26+
invokeCallbackWithArrayLists(callback, i);
27+
}
28+
}
29+
});
30+
thread.start();
31+
}
32+
33+
/**
34+
* Calls the callback synchronously from the current thread.
35+
* @param callback The callback to invoke
36+
* @param times Number of times to call the callback (default 50)
37+
*/
38+
public static void callSynchronously(Callback callback, int times) {
39+
for (int i = 0; i < times; i++) {
40+
invokeCallbackWithArrayLists(callback, i);
41+
}
42+
}
43+
44+
/**
45+
* Helper method that creates 10 ArrayLists and invokes the callback with them.
46+
* Each ArrayList contains some data based on the iteration number.
47+
*/
48+
private static void invokeCallbackWithArrayLists(Callback callback, int iteration) {
49+
ArrayList<Integer> list1 = new ArrayList<>();
50+
ArrayList<Integer> list2 = new ArrayList<>();
51+
ArrayList<Integer> list3 = new ArrayList<>();
52+
ArrayList<Integer> list4 = new ArrayList<>();
53+
ArrayList<Integer> list5 = new ArrayList<>();
54+
ArrayList<Integer> list6 = new ArrayList<>();
55+
ArrayList<Integer> list7 = new ArrayList<>();
56+
ArrayList<Integer> list8 = new ArrayList<>();
57+
ArrayList<Integer> list9 = new ArrayList<>();
58+
ArrayList<Integer> list10 = new ArrayList<>();
59+
60+
// Add some data to each list
61+
for (int i = 0; i < 5; i++) {
62+
list1.add(iteration * 10 + i);
63+
list2.add(iteration * 10 + i + 1);
64+
list3.add(iteration * 10 + i + 2);
65+
list4.add(iteration * 10 + i + 3);
66+
list5.add(iteration * 10 + i + 4);
67+
list6.add(iteration * 10 + i + 5);
68+
list7.add(iteration * 10 + i + 6);
69+
list8.add(iteration * 10 + i + 7);
70+
list9.add(iteration * 10 + i + 8);
71+
list10.add(iteration * 10 + i + 9);
72+
}
73+
74+
callback.invoke(list1, list2, list3, list4, list5, list6, list7, list8, list9, list10);
75+
}
76+
}

test-app/runtime/src/main/java/com/tns/Runtime.java

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import java.util.concurrent.FutureTask;
3636
import java.util.concurrent.RunnableFuture;
3737
import java.util.concurrent.atomic.AtomicInteger;
38+
import java.util.Collections;
3839

3940
public class Runtime {
4041
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)
103104
"Primitive types need to be manually wrapped in their respective Object wrappers.\n" +
104105
"If you are creating an instance of an inner class, make sure to always provide reference to the outer `this` as the first argument.";
105106

106-
private HashMap<Integer, Object> strongInstances = new HashMap<>();
107+
private Map<Integer, Object> strongInstances = new HashMap<>();
107108

108-
private HashMap<Integer, WeakReference<Object>> weakInstances = new HashMap<>();
109+
private Map<Integer, WeakReference<Object>> weakInstances = new HashMap<>();
109110

110-
private NativeScriptHashMap<Object, Integer> strongJavaObjectToID = new NativeScriptHashMap<Object, Integer>();
111+
private Map<Object, Integer> strongJavaObjectToID = new NativeScriptHashMap<Object, Integer>();
111112

112-
private NativeScriptWeakHashMap<Object, Integer> weakJavaObjectToID = new NativeScriptWeakHashMap<Object, Integer>();
113+
private Map<Object, Integer> weakJavaObjectToID = new NativeScriptWeakHashMap<Object, Integer>();
113114

114-
private final Map<Class<?>, JavaScriptImplementation> loadedJavaScriptExtends = new HashMap<Class<?>, JavaScriptImplementation>();
115+
private Map<Class<?>, JavaScriptImplementation> loadedJavaScriptExtends = new HashMap<Class<?>, JavaScriptImplementation>();
115116

116117
private final java.lang.Runtime dalvikRuntime = java.lang.Runtime.getRuntime();
117118

@@ -215,6 +216,16 @@ public Runtime(StaticConfiguration config, DynamicConfiguration dynamicConfigura
215216
if (dynamicConfiguration.mainThreadScheduler != null) {
216217
this.mainThreadHandler = dynamicConfiguration.mainThreadScheduler.getHandler();
217218
}
219+
// if multithreadedJS, make all maps concurrent or synchronized:
220+
if (config.appConfig.getEnableMultithreadedJavascript()) {
221+
this.strongInstances = new ConcurrentHashMap<>();
222+
this.weakInstances = new ConcurrentHashMap<>();
223+
// TODO: can't use a ConcurrentHashMap for loadedJavaScriptExtends because it loads null objects, which aren't supported
224+
// either leave it like this or create a separate set for null caches
225+
this.loadedJavaScriptExtends = Collections.synchronizedMap(new HashMap<>());
226+
this.strongJavaObjectToID = Collections.synchronizedMap(new NativeScriptHashMap<>());
227+
this.weakJavaObjectToID = Collections.synchronizedMap(new NativeScriptWeakHashMap<>());
228+
}
218229

219230
classResolver = new ClassResolver(classStorageService);
220231
currentRuntime.set(this);

0 commit comments

Comments
 (0)