From 5618c3ff09419b2eb0f8c50aabea1afacc96a39b Mon Sep 17 00:00:00 2001 From: Janic Duplessis Date: Wed, 13 Jul 2016 22:44:19 -0700 Subject: [PATCH] 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 https://github.com/facebook/react-native/pull/8734 Differential Revision: D3560818 fbshipit-source-id: a28d3092377a7fd4331647148d40fe69e4198c7e --- Examples/UIExplorer/js/TimerExample.js | 14 ------ React/Modules/RCTTiming.m | 67 ++++++++++++++++++++------ 2 files changed, 52 insertions(+), 29 deletions(-) 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]; } }