Permalink
Browse files

Initial implementation of requestIdleCallback on iOS

Summary:
iOS follow up to #8569. This currently depends on the Android PR since it contains the JS implementation, only review the last commit. Just putting this out here for visibility, don't merge this before the Android PR.

**Test plan**
Tested by running a background task that burns all remaining idle time (see UIExplorer example).

Tested that native only calls into JS when there are pending idle callbacks.

Tested that timers are executed before idle callback.
Closes #8734

Differential Revision: D3560818

fbshipit-source-id: a28d3092377a7fd4331647148d40fe69e4198c7e
  • Loading branch information...
1 parent 18394fb commit 5618c3ff09419b2eb0f8c50aabea1afacc96a39b @janicduplessis janicduplessis committed with Facebook Github Bot 6 Jul 14, 2016
Showing with 52 additions and 29 deletions.
  1. +0 −14 Examples/UIExplorer/js/TimerExample.js
  2. +52 −15 React/Modules/RCTTiming.m
@@ -57,20 +57,6 @@ var RequestIdleCallbackTester = React.createClass({
render() {
return (
<View>
- {Platform.OS === 'ios' ? this._renderIOS() : this._renderAndroid()}
- </View>
- );
- },
-
- _renderIOS() {
- return (
- <Text>Not implemented on iOS, falls back to requestAnimationFrame</Text>
- );
- },
-
- _renderAndroid() {
- return (
- <View>
<UIExplorerButton onPress={this._run.bind(this, false)}>
Run requestIdleCallback
</UIExplorerButton>
@@ -16,6 +16,10 @@
#import "RCTUtils.h"
static const NSTimeInterval kMinimumSleepInterval = 1;
+// The duration of a frame. This assumes that we want to run at 60 fps.
+static const NSTimeInterval kFrameDuration = 1.0 / 60.0;
+// The minimum time left in a frame to trigger the idle callback.
+static const NSTimeInterval kIdleCallbackFrameDeadline = 0.001;
@interface _RCTTimer : NSObject
@@ -87,6 +91,7 @@ @implementation RCTTiming
{
NSMutableDictionary<NSNumber *, _RCTTimer *> *_timers;
NSTimer *_sleepTimer;
+ BOOL _sendIdleEvents;
}
@synthesize bridge = _bridge;
@@ -133,6 +138,14 @@ - (dispatch_queue_t)methodQueue
return RCTJSThread;
}
+- (NSDictionary *)constantsToExport
+{
+ return @{
+ @"frameDuration": @(kFrameDuration * 1000),
+ @"idleCallbackFrameDeadline": @(kIdleCallbackFrameDeadline * 1000),
+ };
+}
+
- (void)invalidate
{
[self stopTimers];
@@ -151,7 +164,7 @@ - (void)stopTimers
- (void)startTimers
{
- if (!_bridge || _timers.count == 0) {
+ if (!_bridge || ![self hasPendingTimers]) {
return;
}
@@ -163,6 +176,11 @@ - (void)startTimers
}
}
+- (BOOL)hasPendingTimers
+{
+ return _sendIdleEvents || _timers.count > 0;
+}
+
- (void)didUpdateFrame:(__unused RCTFrameUpdate *)update
{
NSDate *nextScheduledTarget = [NSDate distantFuture];
@@ -181,22 +199,31 @@ - (void)didUpdateFrame:(__unused RCTFrameUpdate *)update
// Call timers that need to be called
if (timersToCall.count > 0) {
[_bridge enqueueJSCall:@"JSTimersExecution.callTimers" args:@[timersToCall]];
-
- // If we call at least one timer this frame, don't switch to a paused state yet, so if
- // in response to this timer another timer is scheduled, we don't pause and unpause
- // the displaylink frivolously.
- return;
}
- // No need to call the pauseCallback as RCTDisplayLink will ask us about our paused
- // status immediately after completing this call
- if (_timers.count == 0) {
- _paused = YES;
+ if (_sendIdleEvents) {
+ NSTimeInterval frameElapsed = (CACurrentMediaTime() - update.timestamp);
+ if (kFrameDuration - frameElapsed >= kIdleCallbackFrameDeadline) {
+ NSTimeInterval currentTimestamp = [[NSDate date] timeIntervalSince1970];
+ NSNumber *absoluteFrameStartMS = @((currentTimestamp - frameElapsed) * 1000);
+ [_bridge enqueueJSCall:@"JSTimersExecution.callIdleCallbacks" args:@[absoluteFrameStartMS]];
+ }
}
- // If the next timer is more than 1 second out, pause and schedule an NSTimer;
- else if ([nextScheduledTarget timeIntervalSinceNow] > kMinimumSleepInterval) {
- [self scheduleSleepTimer:nextScheduledTarget];
- _paused = YES;
+
+ // Switch to a paused state only if we didn't call any timer this frame, so if
+ // in response to this timer another timer is scheduled, we don't pause and unpause
+ // the displaylink frivolously.
+ if (!_sendIdleEvents && timersToCall.count == 0) {
+ // No need to call the pauseCallback as RCTDisplayLink will ask us about our paused
+ // status immediately after completing this call
+ if (_timers.count == 0) {
+ _paused = YES;
+ }
+ // If the next timer is more than 1 second out, pause and schedule an NSTimer;
+ else if ([nextScheduledTarget timeIntervalSinceNow] > kMinimumSleepInterval) {
+ [self scheduleSleepTimer:nextScheduledTarget];
+ _paused = YES;
+ }
}
}
@@ -268,7 +295,17 @@ - (void)timerDidFire
RCT_EXPORT_METHOD(deleteTimer:(nonnull NSNumber *)timerID)
{
[_timers removeObjectForKey:timerID];
- if (_timers.count == 0) {
+ if (![self hasPendingTimers]) {
+ [self stopTimers];
+ }
+}
+
+RCT_EXPORT_METHOD(setSendIdleEvents:(BOOL)sendIdleEvents)
+{
+ _sendIdleEvents = sendIdleEvents;
+ if (sendIdleEvents) {
+ [self startTimers];
+ } else if (![self hasPendingTimers]) {
[self stopTimers];
}
}

0 comments on commit 5618c3f

Please sign in to comment.