[TIMOB-26673] iOS: Race conditions in async APIs (e.g. timers)
GitHub Issue | n/a |
---|---|
Type | Bug |
Priority | High |
Status | Closed |
Resolution | Fixed |
Resolution Date | 2019-01-11T19:46:43.000+0000 |
Affected Version/s | Release 8.0.0 |
Fix Version/s | Release 8.0.0 |
Components | iOS |
Labels | n/a |
Reporter | Jan Vennemann |
Assignee | Jan Vennemann |
Created | 2018-12-20T17:45:08.000+0000 |
Updated | 2019-01-15T15:13:15.000+0000 |
Description
Async functions can trigger a race conditions which leads to an internal dead lock inside JavaScriptCore. This happens because we often reschedule block execution using [TiThreadPerformOnMainThread](https://github.com/appcelerator/titanium_mobile/blob/33498aeaddac7409a3f411a60455bfc5ddaeabf6/iphone/TitaniumKit/TitaniumKit/Sources/API/TiBase.m#L166).
Note that these race conditions only happen if a lot of async stuff happens at the same time. This example uses a timer with a 5ms interval to force the deadlock to happen. It is very unlikely to happen under normal conditions.
*Steps to reproduce the behavior*
Run the following code in a classic app on an iOS device:
Now, this function contains an async function, namely
Inside the callback (2) we make use of
The timer callback (1) is now called which also want's to use
let counter = 0;
const win = Ti.UI.createWindow({ layout: 'vertical' });
const statusLabel = Ti.UI.createLabel({ top: 100, text: 'Running ...' });
win.add(statusLabel);
const counterLabel = Ti.UI.createLabel({ top: 5 });
win.add(counterLabel);
const testButton = Ti.UI.createButton({ top: 6, title: 'Click me!' });
testButton.addEventListener('click', () => Ti.API.info('expected'));
win.add(testButton);
function triggerRaceCondition() {
counterLabel.text = counter++;
console.log([${counter}] requestUserNotificationSettings
);
Ti.App.iOS.UserNotificationCenter.requestUserNotificationSettings(e => {
console.log([${counter}] requestUserNotificationSettings callback
);
});
}
const triggerInterval = setInterval(() => triggerRaceCondition(), 5);
setTimeout(() => {
clearInterval(triggerInterval);
statusLabel.text = 'Finished!';
}, 1000);
win.open();
*Actual behavior*
The whole app will freeze after a few iterations. The counter will not increase anymore and the button will not accept clicks.
*Expected behavior*
The counter should increase for about one second and then stop. The status should switch from "Running" to "Finished". The button should accept click events and print "expected" to the console.
*Additional information*
This is caused by [TiThreadPerformOnMainThread](https://github.com/appcelerator/titanium_mobile/blob/33498aeaddac7409a3f411a60455bfc5ddaeabf6/iphone/TitaniumKit/TitaniumKit/Sources/API/TiBase.m#L166) which reschedules a block on the main queue when not running on main thread.
In this particular test case various things happen which ultimately lead to the deadlock inside JSCore:
A simple workaround for this specific test case using
UNNotificationCenter
is to wrap calling the callback inTiThreadPerformOnMainThread
. This will schedule the complete callback execution on the main thread and not every single JS statement made in the callback. This should also be a lot more performant since we do not need to constantly switch threads.PR: https://github.com/appcelerator/titanium_mobile/pull/10556 This fixes the issues with
UNNotificationCenter
. We may need to scan our SDK for similar issues.8_0_X backport done in https://github.com/appcelerator/titanium_mobile/pull/10584
FR Passed. Waiting for merge conflicts to be resolved for merge
PR's Merged.
Closing ticket, Verified fix in SDK Version 8.1.0.v20190115054502 and SDK version 8.0.0.v20190114160512. Test and other information can be found at: Master: https://github.com/appcelerator/titanium_mobile/pull/10556 8_0_X: https://github.com/appcelerator/titanium_mobile/pull/10584