Skip to content

Commit

Permalink
feat(JSTimers): Add setIdleCallback support (microsoft#1344)
Browse files Browse the repository at this point in the history
This changeset adds `setIdleCallback` support, now that we have a choreographer that can track frame deadlines.

There still seems to be an issue with getting idle callbacks on a regular basis. E.g., the "background" example in RNTester doesn't seem to fire very often (it only fires if there is at least 5ms of frame time remaining).

Fixes microsoft#917
  • Loading branch information
rozele authored Sep 14, 2017
1 parent 401129d commit f8e5879
Show file tree
Hide file tree
Showing 4 changed files with 156 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand All @@ -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]);
}
}
}
9 changes: 9 additions & 0 deletions ReactWindows/ReactNative.Shared/Modules/Core/JSTimers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,14 @@ public void callTimers(IList<int> timerIds)
{
Invoke(timerIds);
}

/// <summary>
/// Calls the idle callbacks with the current frame time.
/// </summary>
/// <param name="frameTime">The frame time.</param>
public void callIdleCallbacks(long frameTime)
{
Invoke(frameTime);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,12 @@ private ReactChoreographer() { }
/// </summary>
public event EventHandler<FrameEventArgs> JavaScriptEventsCallback;

/// <summary>
/// Event used to trigger the idle callback. Called after all UI work has been
/// dispatched to JavaScript.
/// </summary>
public event EventHandler<FrameEventArgs> IdleCallback;

/// <summary>
/// The choreographer instance.
/// </summary>
Expand Down Expand Up @@ -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)
{
Expand Down
120 changes: 120 additions & 0 deletions ReactWindows/ReactNative.Shared/Modules/Core/Timing.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using ReactNative.Collections;
using System;
using System.Collections.Generic;
using System.Reactive.Disposables;
using System.Threading;

namespace ReactNative.Modules.Core
Expand All @@ -11,13 +12,23 @@ namespace ReactNative.Modules.Core
/// </summary>
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<TimerData> _timers;

private readonly SerialDisposable _idleCancellationDisposable = new SerialDisposable();

private JSTimers _jsTimersModule;
private bool _suspended;

private bool _sendIdleEvents;

/// <summary>
/// Instantiates the <see cref="Timing"/> module.
/// </summary>
Expand Down Expand Up @@ -57,6 +68,14 @@ public void OnSuspend()
{
_suspended = true;
ReactChoreographer.Instance.JavaScriptEventsCallback -= DoFrameSafe;

lock (_idleGate)
{
if (_sendIdleEvents)
{
ReactChoreographer.Instance.IdleCallback -= DoFrameIdleCallbackSafe;
}
}
}

/// <summary>
Expand All @@ -66,6 +85,14 @@ public void OnResume()
{
_suspended = false;
ReactChoreographer.Instance.JavaScriptEventsCallback += DoFrameSafe;

lock (_idleGate)
{
if (_sendIdleEvents)
{
ReactChoreographer.Instance.IdleCallback += DoFrameIdleCallbackSafe;
}
}
}

/// <summary>
Expand All @@ -74,6 +101,14 @@ public void OnResume()
public void OnDestroy()
{
ReactChoreographer.Instance.JavaScriptEventsCallback -= DoFrameSafe;

lock (_idleGate)
{
if (_sendIdleEvents)
{
ReactChoreographer.Instance.IdleCallback -= DoFrameIdleCallbackSafe;
}
}
}

/// <summary>
Expand Down Expand Up @@ -130,6 +165,40 @@ public void deleteTimer(int timerId)
}
}

/// <summary>
/// Enable or disable idle events.
/// </summary>
/// <param name="sendIdleEvents">
/// <code>true</code> if idle events should be enabled, otherwise
/// <code>false</code>.
/// </param>
[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);
}
}
}

/// <summary>
/// Called before a <see cref="IReactInstance"/> is disposed.
/// </summary>
public override void OnReactInstanceDispose()
{
_idleCancellationDisposable.Dispose();
}

private void DoFrameSafe(object sender, object e)
{
try
Expand Down Expand Up @@ -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<JSTimers>()
.callIdleCallbacks(frameStartTime.ToUnixTimeMilliseconds());
}
}

struct TimerData : IEquatable<TimerData>
{
private readonly bool _repeat;
Expand Down

0 comments on commit f8e5879

Please sign in to comment.