diff --git a/Examples/UIExplorer/js/TimerExample.js b/Examples/UIExplorer/js/TimerExample.js index 7b577821f..97d4b9a32 100644 --- a/Examples/UIExplorer/js/TimerExample.js +++ b/Examples/UIExplorer/js/TimerExample.js @@ -55,20 +55,6 @@ var RequestIdleCallbackTester = React.createClass({ }, render() { - return ( - - {Platform.OS === 'ios' ? this._renderIOS() : this._renderAndroid()} - - ); - }, - - _renderIOS() { - return ( - Not implemented on iOS, falls back to requestAnimationFrame - ); - }, - - _renderAndroid() { return ( diff --git a/React/Modules/RCTTiming.m b/React/Modules/RCTTiming.m index a4038f1b9..6e5cea8d0 100644 --- a/React/Modules/RCTTiming.m +++ b/React/Modules/RCTTiming.m @@ -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 @@ static const NSTimeInterval kMinimumSleepInterval = 1; { NSMutableDictionary *_timers; NSTimer *_sleepTimer; + BOOL _sendIdleEvents; } @synthesize bridge = _bridge; @@ -133,6 +138,14 @@ RCT_EXPORT_MODULE() return RCTJSThread; } +- (NSDictionary *)constantsToExport +{ + return @{ + @"frameDuration": @(kFrameDuration * 1000), + @"idleCallbackFrameDeadline": @(kIdleCallbackFrameDeadline * 1000), + }; +} + - (void)invalidate { [self stopTimers]; @@ -151,7 +164,7 @@ RCT_EXPORT_MODULE() - (void)startTimers { - if (!_bridge || _timers.count == 0) { + if (!_bridge || ![self hasPendingTimers]) { return; } @@ -163,6 +176,11 @@ RCT_EXPORT_MODULE() } } +- (BOOL)hasPendingTimers +{ + return _sendIdleEvents || _timers.count > 0; +} + - (void)didUpdateFrame:(__unused RCTFrameUpdate *)update { NSDate *nextScheduledTarget = [NSDate distantFuture]; @@ -181,22 +199,31 @@ RCT_EXPORT_MODULE() // 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 @@ RCT_EXPORT_METHOD(createTimer:(nonnull NSNumber *)callbackID 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]; } }