diff --git a/ReactWindows/ReactNative.Shared.Tests/Modules/Core/JSTimersTests.cs b/ReactWindows/ReactNative.Shared.Tests/Modules/Core/JSTimersTests.cs index 3ee3d4bf1d5..cb8465db4b2 100644 --- a/ReactWindows/ReactNative.Shared.Tests/Modules/Core/JSTimersTests.cs +++ b/ReactWindows/ReactNative.Shared.Tests/Modules/Core/JSTimersTests.cs @@ -6,7 +6,7 @@ namespace ReactNative.Tests.Modules.Core [TestFixture] public class JSTimersTests { - public void JSTimers_Invoke() + public void JSTimers_callTimers_Invoke() { var module = new JSTimers(); @@ -24,5 +24,24 @@ public void JSTimers_Invoke() Assert.AreEqual(1, args.Length); Assert.AreSame(ids, args[0]); } + + public void JSTimers_callIdleCallbacks_Invoke() + { + var module = new JSTimers(); + + var name = default(string); + var args = default(object[]); + module.InvocationHandler = new MockInvocationHandler((n, a) => + { + name = n; + args = a; + }); + + var frameTime = 42L; + module.callIdleCallbacks(frameTime); + Assert.AreEqual(nameof(JSTimers.callIdleCallbacks), name); + Assert.AreEqual(1, args.Length); + Assert.AreSame(frameTime, args[0]); + } } } diff --git a/ReactWindows/ReactNative.Shared/Modules/Core/JSTimers.cs b/ReactWindows/ReactNative.Shared/Modules/Core/JSTimers.cs index 4f9732b81e3..e61f0caf1f9 100644 --- a/ReactWindows/ReactNative.Shared/Modules/Core/JSTimers.cs +++ b/ReactWindows/ReactNative.Shared/Modules/Core/JSTimers.cs @@ -16,5 +16,14 @@ public void callTimers(IList timerIds) { Invoke(timerIds); } + + /// + /// Calls the idle callbacks with the current frame time. + /// + /// The frame time. + public void callIdleCallbacks(long frameTime) + { + Invoke(frameTime); + } } } diff --git a/ReactWindows/ReactNative.Shared/Modules/Core/ReactChoreographer.cs b/ReactWindows/ReactNative.Shared/Modules/Core/ReactChoreographer.cs index 60443faec41..a4afc6043d4 100644 --- a/ReactWindows/ReactNative.Shared/Modules/Core/ReactChoreographer.cs +++ b/ReactWindows/ReactNative.Shared/Modules/Core/ReactChoreographer.cs @@ -53,6 +53,12 @@ private ReactChoreographer() { } /// public event EventHandler JavaScriptEventsCallback; + /// + /// Event used to trigger the idle callback. Called after all UI work has been + /// dispatched to JavaScript. + /// + public event EventHandler IdleCallback; + /// /// The choreographer instance. /// @@ -166,6 +172,7 @@ private void OnRendering(object sender, object e) DispatchUICallback?.Invoke(sender, _frameEventArgs); NativeAnimatedCallback?.Invoke(sender, _frameEventArgs); JavaScriptEventsCallback?.Invoke(sender, _frameEventArgs); + IdleCallback?.Invoke(sender, _frameEventArgs); lock (_gate) { diff --git a/ReactWindows/ReactNative.Shared/Modules/Core/Timing.cs b/ReactWindows/ReactNative.Shared/Modules/Core/Timing.cs index c18b1e3b433..42ace5d6320 100644 --- a/ReactWindows/ReactNative.Shared/Modules/Core/Timing.cs +++ b/ReactWindows/ReactNative.Shared/Modules/Core/Timing.cs @@ -2,6 +2,7 @@ using ReactNative.Collections; using System; using System.Collections.Generic; +using System.Reactive.Disposables; using System.Threading; namespace ReactNative.Modules.Core @@ -11,13 +12,23 @@ namespace ReactNative.Modules.Core /// public class Timing : ReactContextNativeModuleBase, ILifecycleEventListener { + private const string IdleChoreographerKey = nameof(Timing) + "_Idle"; + + private static readonly TimeSpan s_frameDuration = TimeSpan.FromTicks(166666); + private static readonly TimeSpan s_idleCallbackFrameDeadline = TimeSpan.FromMilliseconds(1); + private readonly object _gate = new object(); + private readonly object _idleGate = new object(); private readonly HeapBasedPriorityQueue _timers; + private readonly SerialDisposable _idleCancellationDisposable = new SerialDisposable(); + private JSTimers _jsTimersModule; private bool _suspended; + private bool _sendIdleEvents; + /// /// Instantiates the module. /// @@ -57,6 +68,14 @@ public void OnSuspend() { _suspended = true; ReactChoreographer.Instance.JavaScriptEventsCallback -= DoFrameSafe; + + lock (_idleGate) + { + if (_sendIdleEvents) + { + ReactChoreographer.Instance.IdleCallback -= DoFrameIdleCallbackSafe; + } + } } /// @@ -66,6 +85,14 @@ public void OnResume() { _suspended = false; ReactChoreographer.Instance.JavaScriptEventsCallback += DoFrameSafe; + + lock (_idleGate) + { + if (_sendIdleEvents) + { + ReactChoreographer.Instance.IdleCallback += DoFrameIdleCallbackSafe; + } + } } /// @@ -74,6 +101,14 @@ public void OnResume() public void OnDestroy() { ReactChoreographer.Instance.JavaScriptEventsCallback -= DoFrameSafe; + + lock (_idleGate) + { + if (_sendIdleEvents) + { + ReactChoreographer.Instance.IdleCallback -= DoFrameIdleCallbackSafe; + } + } } /// @@ -130,6 +165,40 @@ public void deleteTimer(int timerId) } } + /// + /// Enable or disable idle events. + /// + /// + /// true if idle events should be enabled, otherwise + /// false. + /// + [ReactMethod] + public void setSendIdleEvents(bool sendIdleEvents) + { + lock (_idleGate) + { + _sendIdleEvents = sendIdleEvents; + if (_sendIdleEvents) + { + ReactChoreographer.Instance.IdleCallback += DoFrameIdleCallbackSafe; + ReactChoreographer.Instance.ActivateCallback(IdleChoreographerKey); + } + else + { + ReactChoreographer.Instance.IdleCallback -= DoFrameIdleCallbackSafe; + ReactChoreographer.Instance.DeactivateCallback(IdleChoreographerKey); + } + } + } + + /// + /// Called before a is disposed. + /// + public override void OnReactInstanceDispose() + { + _idleCancellationDisposable.Dispose(); + } + private void DoFrameSafe(object sender, object e) { try @@ -178,6 +247,57 @@ private void DoFrame(object sender, object e) } } + private void DoFrameIdleCallbackSafe(object sender, FrameEventArgs e) + { + try + { + DoFrameIdleCallback(sender, e); + } + catch (Exception ex) + { + Context.HandleException(ex); + } + } + + private void DoFrameIdleCallback(object sender, FrameEventArgs e) + { + if (Volatile.Read(ref _suspended)) + { + return; + } + + var cancellationDisposable = new CancellationDisposable(); + _idleCancellationDisposable.Disposable = cancellationDisposable; + Context.RunOnJavaScriptQueueThread(() => DoIdleCallback(e.FrameTime, cancellationDisposable.Token)); + } + + private void DoIdleCallback(DateTimeOffset frameTime, CancellationToken token) + { + if (token.IsCancellationRequested) + { + return; + } + + var remainingFrameTime = frameTime - DateTimeOffset.UtcNow; + if (remainingFrameTime < s_idleCallbackFrameDeadline) + { + return; + } + + bool sendIdleEvents; + lock (_idleGate) + { + sendIdleEvents = _sendIdleEvents; + } + + if (sendIdleEvents) + { + var frameStartTime = frameTime - s_frameDuration; + Context.GetJavaScriptModule() + .callIdleCallbacks(frameStartTime.ToUnixTimeMilliseconds()); + } + } + struct TimerData : IEquatable { private readonly bool _repeat;