Skip to content

Commit

Permalink
fix(Text): Add separate layout thread with dispatcher access
Browse files Browse the repository at this point in the history
Text measurement was a bit of a performance bottleneck due to context switching from the native module thread to the dispatcher thread.

Eventually, we may want to implement text measurement that does not invole cloning the RichTextBox (e.g., using DWrite APIs or Win2D), but this is progress for now.

We create the layout thread from a static secondary CoreApplicationView with a Window that is never activated.
  • Loading branch information
rozele committed Aug 9, 2016
1 parent 2f29e8c commit 38c0726
Show file tree
Hide file tree
Showing 13 changed files with 242 additions and 134 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -26,19 +26,22 @@ public void MessageQueueThread_ArgumentChecks()
public void MessageQueueThread_CreateUiThread_ThrowsNotSupported()
{
AssertEx.Throws<NotSupportedException>(() => MessageQueueThreadSpec.Create("ui", MessageQueueThreadKind.DispatcherThread));
AssertEx.Throws<NotSupportedException>(() => MessageQueueThreadSpec.Create("layout", MessageQueueThreadKind.LayoutThread));
}

[TestMethod]
public async Task MessageQueueThread_IsOnThread()
{
var thrown = 0;
var uiThread = await CallOnDispatcherAsync(() => MessageQueueThread.Create(MessageQueueThreadSpec.DispatcherThreadSpec, ex => thrown++));
var layoutThread = await CallOnDispatcherAsync(() => MessageQueueThread.Create(MessageQueueThreadSpec.LayoutThreadSpec, ex => thrown++));
var backgroundThread = MessageQueueThread.Create(MessageQueueThreadSpec.Create("background", MessageQueueThreadKind.BackgroundSingleThread), ex => thrown++);
var taskPoolThread = MessageQueueThread.Create(MessageQueueThreadSpec.Create("any", MessageQueueThreadKind.BackgroundAnyThread), ex => thrown++);

var queueThreads = new[]
{
uiThread,
layoutThread,
backgroundThread,
taskPoolThread
};
Expand Down Expand Up @@ -69,12 +72,14 @@ public async Task MessageQueueThread_HandlesException()
});

var uiThread = await CallOnDispatcherAsync(() => MessageQueueThread.Create(MessageQueueThreadSpec.DispatcherThreadSpec, handler));
var layoutThread = await CallOnDispatcherAsync(() => MessageQueueThread.Create(MessageQueueThreadSpec.LayoutThreadSpec, handler));
var backgroundThread = MessageQueueThread.Create(MessageQueueThreadSpec.Create("background", MessageQueueThreadKind.BackgroundSingleThread), handler);
var taskPoolThread = MessageQueueThread.Create(MessageQueueThreadSpec.Create("any", MessageQueueThreadKind.BackgroundAnyThread), handler);

var queueThreads = new[]
{
uiThread,
layoutThread,
backgroundThread,
taskPoolThread
};
Expand All @@ -90,16 +95,18 @@ public async Task MessageQueueThread_HandlesException()
[TestMethod]
public async Task MessageQueueThread_OneAtATime()
{
var uiThread = await CallOnDispatcherAsync(() => MessageQueueThread.Create(MessageQueueThreadSpec.DispatcherThreadSpec, ex => { Assert.Fail(); }));
var backgroundThread = MessageQueueThread.Create(MessageQueueThreadSpec.Create("background", MessageQueueThreadKind.BackgroundSingleThread), ex => { Assert.Fail(); });
var taskPoolThread = MessageQueueThread.Create(MessageQueueThreadSpec.Create("any", MessageQueueThreadKind.BackgroundAnyThread), ex => { Assert.Fail(); });
var uiThread = await CallOnDispatcherAsync(() => MessageQueueThread.Create(MessageQueueThreadSpec.DispatcherThreadSpec, ex => Assert.Fail()));
var layoutThread = await CallOnDispatcherAsync(() => MessageQueueThread.Create(MessageQueueThreadSpec.LayoutThreadSpec, ex => Assert.Fail()));
var backgroundThread = MessageQueueThread.Create(MessageQueueThreadSpec.Create("background", MessageQueueThreadKind.BackgroundSingleThread), ex => Assert.Fail());
var taskPoolThread = MessageQueueThread.Create(MessageQueueThreadSpec.Create("any", MessageQueueThreadKind.BackgroundAnyThread), ex => Assert.Fail());

var enter = new AutoResetEvent(false);
var exit = new AutoResetEvent(false);

var queueThreads = new[]
{
uiThread,
layoutThread,
backgroundThread,
taskPoolThread
};
Expand All @@ -124,13 +131,15 @@ public async Task MessageQueueThread_OneAtATime()
[TestMethod]
public async Task MessageQueueThread_Dispose()
{
var uiThread = await CallOnDispatcherAsync(() => MessageQueueThread.Create(MessageQueueThreadSpec.DispatcherThreadSpec, ex => { Assert.Fail(); }));
var backgroundThread = MessageQueueThread.Create(MessageQueueThreadSpec.Create("background", MessageQueueThreadKind.BackgroundSingleThread), ex => { Assert.Fail(); });
var taskPoolThread = MessageQueueThread.Create(MessageQueueThreadSpec.Create("any", MessageQueueThreadKind.BackgroundAnyThread), ex => { Assert.Fail(); });
var uiThread = await CallOnDispatcherAsync(() => MessageQueueThread.Create(MessageQueueThreadSpec.DispatcherThreadSpec, ex => Assert.Fail()));
var layoutThread = await CallOnDispatcherAsync(() => MessageQueueThread.Create(MessageQueueThreadSpec.LayoutThreadSpec, ex => Assert.Fail()));
var backgroundThread = MessageQueueThread.Create(MessageQueueThreadSpec.Create("background", MessageQueueThreadKind.BackgroundSingleThread), ex => Assert.Fail());
var taskPoolThread = MessageQueueThread.Create(MessageQueueThreadSpec.Create("any", MessageQueueThreadKind.BackgroundAnyThread), ex => Assert.Fail());

var queueThreads = new[]
{
uiThread,
layoutThread,
backgroundThread,
taskPoolThread
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ public interface IReactQueueConfiguration : IDisposable
/// </summary>
IMessageQueueThread DispatcherQueueThread { get; }

/// <summary>
/// The layout thread.
/// </summary>
IMessageQueueThread LayoutQueueThread { get; }

/// <summary>
/// The native modules thread.
/// </summary>
Expand Down
33 changes: 29 additions & 4 deletions ReactWindows/ReactNative/Bridge/Queue/MessageQueueThread.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using System.Reactive.Subjects;
using System.Threading;
using System.Threading.Tasks;
using Windows.ApplicationModel.Core;
using Windows.Foundation;
using Windows.System.Threading;
using Windows.UI.Core;
Expand Down Expand Up @@ -124,7 +125,9 @@ public static MessageQueueThread Create(
switch (spec.Kind)
{
case MessageQueueThreadKind.DispatcherThread:
return new DispatcherMessageQueueThread(spec.Name, handler);
case MessageQueueThreadKind.LayoutThread:
var isSecondary = spec.Kind == MessageQueueThreadKind.LayoutThread;
return new DispatcherMessageQueueThread(spec.Name, handler, isSecondary);
case MessageQueueThreadKind.BackgroundSingleThread:
return new SingleBackgroundMessageQueueThread(spec.Name, handler);
case MessageQueueThreadKind.BackgroundAnyThread:
Expand All @@ -137,20 +140,28 @@ public static MessageQueueThread Create(

class DispatcherMessageQueueThread : MessageQueueThread
{
private static readonly CoreApplicationView s_secondaryView = CoreApplication.CreateNewView();
private static readonly IObserver<Action> s_nop = Observer.Create<Action>(_ => { });

private readonly bool _isSecondary;
private readonly Subject<Action> _actionSubject;
private readonly IDisposable _subscription;

private IObserver<Action> _actionObserver;

public DispatcherMessageQueueThread(string name, Action<Exception> handler)
public DispatcherMessageQueueThread(string name, Action<Exception> handler, bool isSecondary)
: base(name)
{
_isSecondary = isSecondary;
_actionSubject = new Subject<Action>();
_actionObserver = _actionSubject;

var dispatcher = isSecondary
? s_secondaryView.Dispatcher
: CoreApplication.GetCurrentView().Dispatcher;

_subscription = _actionSubject
.ObserveOnDispatcher()
.ObserveOn(dispatcher)
.Subscribe(action =>
{
try
Expand All @@ -171,7 +182,7 @@ protected override void Enqueue(Action action)

protected override bool IsOnThreadCore()
{
return CoreWindow.GetForCurrentThread().Dispatcher != null;
return GetApplicationView() != null;
}

protected override void Dispose(bool disposing)
Expand All @@ -181,6 +192,20 @@ protected override void Dispose(bool disposing)
_actionSubject.Dispose();
_subscription.Dispose();
}

private CoreApplicationView GetApplicationView()
{
if (_isSecondary && s_secondaryView.Dispatcher.HasThreadAccess)
{
return s_secondaryView;
}
else if (!_isSecondary)
{
return CoreApplication.GetCurrentView();
}

return null;
}
}

class SingleBackgroundMessageQueueThread : MessageQueueThread
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ public enum MessageQueueThreadKind
/// Dispatcher thread type.
/// </summary>
DispatcherThread,

/// <summary>
/// Layout thread type.
/// </summary>
LayoutThread,

/// <summary>
/// Single background thread type.
Expand Down
12 changes: 11 additions & 1 deletion ReactWindows/ReactNative/Bridge/Queue/MessageQueueThreadSpec.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ private MessageQueueThreadSpec(MessageQueueThreadKind kind, string name)
/// </summary>
public static MessageQueueThreadSpec DispatcherThreadSpec { get; } = new MessageQueueThreadSpec(MessageQueueThreadKind.DispatcherThread, "main_ui");

/// <summary>
/// Singleton layout <see cref="IMessageQueueThread"/> specification.
/// </summary>
public static MessageQueueThreadSpec LayoutThreadSpec { get; } = new MessageQueueThreadSpec(MessageQueueThreadKind.LayoutThread, "layout");

/// <summary>
/// Factory for creating <see cref="MessageQueueThreadSpec"/>s.
/// </summary>
Expand All @@ -38,7 +43,12 @@ public static MessageQueueThreadSpec Create(string name, MessageQueueThreadKind
{
if (kind == MessageQueueThreadKind.DispatcherThread)
{
throw new NotSupportedException("Use the singleton MainUiThreadSpec instance.");
throw new NotSupportedException("Use the singleton DispatcherThreadSpec instance.");
}

if (kind == MessageQueueThreadKind.LayoutThread)
{
throw new NotSupportedException("Use the singleton LayoutThreadSpec instance.");
}

return new MessageQueueThreadSpec(kind, name);
Expand Down
20 changes: 19 additions & 1 deletion ReactWindows/ReactNative/Bridge/Queue/ReactQueueConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,18 @@ namespace ReactNative.Bridge.Queue
class ReactQueueConfiguration : IReactQueueConfiguration
{
private readonly MessageQueueThread _dispatcherQueueThread;
private readonly MessageQueueThread _layoutQueueThread;
private readonly MessageQueueThread _nativeModulesQueueThread;
private readonly MessageQueueThread _jsQueueThread;

private ReactQueueConfiguration(
MessageQueueThread dispatcherQueueThread,
MessageQueueThread layoutQueueThread,
MessageQueueThread nativeModulesQueueThread,
MessageQueueThread jsQueueThread)
{
_dispatcherQueueThread = dispatcherQueueThread;
_layoutQueueThread = layoutQueueThread;
_nativeModulesQueueThread = nativeModulesQueueThread;
_jsQueueThread = jsQueueThread;
}
Expand All @@ -35,6 +38,17 @@ public IMessageQueueThread DispatcherQueueThread
}
}

/// <summary>
/// The layout queue thread.
/// </summary>
public IMessageQueueThread LayoutQueueThread
{
get
{
return _layoutQueueThread;
}
}

/// <summary>
/// The native modules thread.
/// </summary>
Expand Down Expand Up @@ -67,6 +81,7 @@ public IMessageQueueThread JavaScriptQueueThread
public void Dispose()
{
_dispatcherQueueThread.Dispose();
_layoutQueueThread.Dispose();
_nativeModulesQueueThread.Dispose();
_jsQueueThread.Dispose();
}
Expand All @@ -84,6 +99,9 @@ public static ReactQueueConfiguration Create(
var dispatcherThreadSpec = MessageQueueThreadSpec.DispatcherThreadSpec;
var dispatcherThread = MessageQueueThread.Create(dispatcherThreadSpec, exceptionHandler);

var layoutThreadSpec = MessageQueueThreadSpec.LayoutThreadSpec;
var layoutThread = MessageQueueThread.Create(layoutThreadSpec, exceptionHandler);

var jsThread = spec.JSQueueThreadSpec != dispatcherThreadSpec
? MessageQueueThread.Create(spec.JSQueueThreadSpec, exceptionHandler)
: dispatcherThread;
Expand All @@ -92,7 +110,7 @@ public static ReactQueueConfiguration Create(
? MessageQueueThread.Create(spec.NativeModulesQueueThreadSpec, exceptionHandler)
: dispatcherThread;

return new ReactQueueConfiguration(dispatcherThread, nativeModulesThread, jsThread);
return new ReactQueueConfiguration(dispatcherThread, layoutThread, nativeModulesThread, jsThread);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@ private ReactQueueConfigurationSpec(
JSQueueThreadSpec = jsQueueThreadSpec;
}

/// <summary>
/// The Layout <see cref="IMessageQueueThread"/> specification.
/// </summary>
public MessageQueueThreadSpec LayoutQueueThreadSpec
{
get;
}

/// <summary>
/// The native modules <see cref="IMessageQueueThread"/> specification.
/// </summary>
Expand All @@ -32,7 +40,7 @@ public MessageQueueThreadSpec JSQueueThreadSpec
{
get;
}

/// <summary>
/// The default <see cref="ReactQueueConfigurationSpec"/> instance.
/// </summary>
Expand All @@ -42,8 +50,8 @@ private static ReactQueueConfigurationSpec CreateDefault()
{
return new Builder()
{
JSQueueThreadSpec = MessageQueueThreadSpec.Create("js", MessageQueueThreadKind.BackgroundSingleThread),
NativeModulesQueueThreadSpec = MessageQueueThreadSpec.Create("native_modules", MessageQueueThreadKind.BackgroundAnyThread),
JSQueueThreadSpec = MessageQueueThreadSpec.Create("js", MessageQueueThreadKind.BackgroundSingleThread),
}
.Build();
}
Expand Down Expand Up @@ -81,7 +89,7 @@ public MessageQueueThreadSpec JSQueueThreadSpec
{
if (_jsQueueThreadSpec != null)
{
throw new InvalidOperationException("Setting native modules queue thread spec multiple times!");
throw new InvalidOperationException("Setting JavaScript queue thread spec multiple times!");
}

_jsQueueThreadSpec = value;
Expand Down
38 changes: 36 additions & 2 deletions ReactWindows/ReactNative/Bridge/ReactContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -207,8 +207,8 @@ public bool IsOnDispatcherQueueThread()
}

/// <summary>
/// Asserts that the current thread is on the React instance native
/// modules queue thread.
/// Asserts that the current thread is on the React instance dispatcher
/// queue thread.
/// </summary>
public void AssertOnDispatcherQueueThread()
{
Expand All @@ -226,6 +226,40 @@ public void RunOnDispatcherQueueThread(Action action)
_reactInstance.QueueConfiguration.DispatcherQueueThread.RunOnQueue(action);
}

/// <summary>
/// Checks if the current thread is on the React instance layout
/// queue thread.
/// </summary>
/// <returns>
/// <b>true</b> if the call is from the layout queue thread,
/// <b>false</b> otherwise.
/// </returns>
public bool IsOnLayoutQueueThread()
{
AssertReactInstance();
return _reactInstance.QueueConfiguration.LayoutQueueThread.IsOnThread();
}

/// <summary>
/// Asserts that the current thread is on the React instance layout
/// queue thread.
/// </summary>
public void AssertOnLayoutQueueThread()
{
AssertReactInstance();
_reactInstance.QueueConfiguration.LayoutQueueThread.AssertOnThread();
}

/// <summary>
/// Enqueues an action on the layout queue thread.
/// </summary>
/// <param name="action">The action.</param>
public void RunOnLayoutQueueThread(Action action)
{
AssertReactInstance();
_reactInstance.QueueConfiguration.LayoutQueueThread.RunOnQueue(action);
}

/// <summary>
/// Checks if the current thread is on the React instance
/// JavaScript queue thread.
Expand Down
Loading

0 comments on commit 38c0726

Please sign in to comment.