Skip to content

Commit

Permalink
feat(Text): Measure Text on secondary Dispatcher
Browse files Browse the repository at this point in the history
Prior to this change, layout was performed on any background thread. Because of this, we had to perform text measurement on the UI thread, which incurred many blocking calls to the UI. This change adds a secondary CoreApplicationView + Dispatcher in UWP and a secondary STA thread in WPF. This thread is characterized as the layout thread, which is only used from the UIManager. All text measurement calls can now be made directly without having to context switch to the main UI thread.
  • Loading branch information
rozele committed Jul 17, 2017
1 parent fcf39fb commit aa15269
Show file tree
Hide file tree
Showing 14 changed files with 327 additions and 188 deletions.
50 changes: 21 additions & 29 deletions ReactWindows/ReactNative.Net46/Views/Text/ReactTextShadowNode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -185,39 +185,31 @@ protected override void MarkUpdated()

private static YogaSize MeasureText(ReactTextShadowNode textNode, YogaNode node, float width, YogaMeasureMode widthMode, float height, YogaMeasureMode heightMode)
{
// This is not a terribly efficient way of projecting the height of
// the text elements. It requires that we have access to the
// dispatcher in order to do measurement, which, for obvious
// reasons, can cause perceived performance issues as it will block
// the UI thread from handling other work.
//
// TODO: determine another way to measure text elements.
var task = DispatcherHelpers.CallOnDispatcher(() =>
// TODO: Measure text with DirectWrite or other API that does not
// require dispatcher access. Currently, we're instantiating a
// second CoreApplicationView (that is never activated) and using
// its Dispatcher thread to calculate layout.
var textBlock = new TextBlock
{
var textBlock = new TextBlock
{
TextAlignment = TextAlignment.Left,
TextWrapping = TextWrapping.Wrap,
TextTrimming = TextTrimming.CharacterEllipsis,
};
textNode.UpdateTextBlockCore(textBlock, true);
TextAlignment = TextAlignment.Left,
TextWrapping = TextWrapping.Wrap,
TextTrimming = TextTrimming.CharacterEllipsis,
};

for (var i = 0; i < textNode.ChildCount; ++i)
{
var child = textNode.GetChildAt(i);
textBlock.Inlines.Add(ReactInlineShadowNodeVisitor.Apply(child));
}
textNode.UpdateTextBlockCore(textBlock, true);

var normalizedWidth = YogaConstants.IsUndefined(width) ? double.PositiveInfinity : width;
var normalizedHeight = YogaConstants.IsUndefined(height) ? double.PositiveInfinity : height;
textBlock.Measure(new Size(normalizedWidth, normalizedHeight));
return MeasureOutput.Make(
(float)Math.Ceiling(textBlock.DesiredSize.Width),
(float)Math.Ceiling(textBlock.DesiredSize.Height));
});
for (var i = 0; i < textNode.ChildCount; ++i)
{
var child = textNode.GetChildAt(i);
textBlock.Inlines.Add(ReactInlineShadowNodeVisitor.Apply(child));
}

return task.Result;
var normalizedWidth = YogaConstants.IsUndefined(width) ? double.PositiveInfinity : width;
var normalizedHeight = YogaConstants.IsUndefined(height) ? double.PositiveInfinity : height;
textBlock.Measure(new Size(normalizedWidth, normalizedHeight));
return MeasureOutput.Make(
(float)Math.Ceiling(textBlock.DesiredSize.Width),
(float)Math.Ceiling(textBlock.DesiredSize.Height));
}

/// <summary>
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
91 changes: 83 additions & 8 deletions ReactWindows/ReactNative.Shared/Bridge/Queue/MessageQueueThread.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
using System.Threading.Tasks;
using static System.FormattableString;
#if WINDOWS_UWP
using Windows.UI.Core;
using Windows.ApplicationModel.Core;
#else
using System.Windows.Threading;
#endif
Expand Down Expand Up @@ -127,7 +127,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 @@ -140,25 +142,78 @@ public static MessageQueueThread Create(

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

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

#if WINDOWS_UWP
private readonly CoreApplicationView _currentApplicationView;
#else
private readonly Thread _currentDispatcherThread;
#endif

private IObserver<Action> _actionObserver;

#if !WINDOWS_UWP
private static readonly ManualResetEvent s_layoutDispatcherReady = new ManualResetEvent(false);
private static readonly Thread s_layoutDispatcherThread;
private static Dispatcher s_layoutDispatcher;

static DispatcherMessageQueueThread()
{
s_layoutDispatcherThread = new Thread(new ThreadStart(() => {
s_layoutDispatcher = Dispatcher.CurrentDispatcher;
s_layoutDispatcherReady.Set();
Dispatcher.Run();
}));

s_layoutDispatcherThread.SetApartmentState(ApartmentState.STA);
s_layoutDispatcherThread.Start();
}
#endif

[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
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;
_subscription = _actionSubject

#if WINDOWS_UWP
.ObserveOnDispatcher()
#if CREATE_LAYOUT_THREAD
_currentApplicationView = isSecondary
? s_secondaryView
: CoreApplication.GetCurrentView();
#else
.ObserveOn(Dispatcher.CurrentDispatcher)
// For DEBUG builds, we use the main UI dispatcher fors both
// layout and dispatcher queue threads because of a limitation
// with the native debugging capabilities.
_currentApplicationView = CoreApplication.GetCurrentView();
#endif
var dispatcher = _currentApplicationView.Dispatcher;
#else
var dispatcher = isSecondary
? s_layoutDispatcher
: Dispatcher.CurrentDispatcher;

_currentDispatcherThread = isSecondary
? s_layoutDispatcherThread
: Dispatcher.CurrentDispatcher.Thread;

if (isSecondary)
{
s_layoutDispatcherReady.WaitOne();
}
#endif

_subscription = _actionSubject
.ObserveOn(dispatcher)
.Subscribe(action =>
{
try
Expand All @@ -180,9 +235,9 @@ protected override void Enqueue(Action action)
protected override bool IsOnThreadCore()
{
#if WINDOWS_UWP
return CoreWindow.GetForCurrentThread().Dispatcher != null;
return GetApplicationView() == _currentApplicationView;
#else
return Thread.CurrentThread == Dispatcher.CurrentDispatcher.Thread;
return Thread.CurrentThread == _currentDispatcherThread;
#endif
}

Expand All @@ -193,6 +248,26 @@ protected override void Dispose(bool disposing)
_actionSubject.Dispose();
_subscription.Dispose();
}

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

return null;
#else
return CoreApplication.GetCurrentView();
#endif
}
#endif
}

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

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

}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,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 @@ -42,6 +47,11 @@ public static MessageQueueThreadSpec Create(string name, MessageQueueThreadKind
throw new NotSupportedException(Invariant($"Use the singleton {nameof(DispatcherThreadSpec)} instance."));
}

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

return new MessageQueueThreadSpec(kind, name);
}
}
Expand Down
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 @@ -85,6 +100,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 @@ -93,7 +111,11 @@ public static ReactQueueConfiguration Create(
? MessageQueueThread.Create(spec.NativeModulesQueueThreadSpec, exceptionHandler)
: dispatcherThread;

return new ReactQueueConfiguration(dispatcherThread, nativeModulesThread, jsThread);
return new ReactQueueConfiguration(
dispatcherThread,
layoutThread,
nativeModulesThread,
jsThread);
}
}
}
34 changes: 34 additions & 0 deletions ReactWindows/ReactNative.Shared/Bridge/ReactContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,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 aa15269

Please sign in to comment.