Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 48 additions & 21 deletions src/Reactor/Hosting/ReactorApp.cs
Original file line number Diff line number Diff line change
Expand Up @@ -638,23 +638,40 @@ private static void RunOnSta(Action action)
/// </summary>
public partial class ReactorApplication : Application, IXamlMetadataProvider
{
private IXamlMetadataProvider? _controlsProvider;

private IXamlMetadataProvider ControlsProvider
// The Reactor library's XAML build pipeline generates
// Microsoft.UI.Reactor.Reactor_XamlTypeInfo.XamlMetaDataProvider — a full provider
// that covers ReactorDefaultResources, XamlControlsResources, ResourceDictionary,
// system primitives, and chains to XamlControlsXamlMetaDataProvider for control
// types. That generated provider is the right primary delegate: it's AOT-safe,
// preserves type registration via compile-time code rather than runtime reflection,
// and correctly handles the schema-only lookups WinUI performs during Application
// startup when theme dictionaries load.
//
// We resolve the generated type at runtime because referencing the generated name
// directly would make the C# pre-compile (run by the XAML compiler itself) fail with
// CS0246 — the generated class doesn't exist yet when that check runs. The
// DynamicDependency keeps the type alive under AOT trimming.
private IXamlMetadataProvider? _reactorProvider;

private IXamlMetadataProvider ReactorProvider => _reactorProvider ??= CreateReactorProvider();

[DynamicDependency(DynamicallyAccessedMemberTypes.PublicConstructors,
"Microsoft.UI.Reactor.Reactor_XamlTypeInfo.XamlMetaDataProvider", "Reactor")]
private static IXamlMetadataProvider CreateReactorProvider()
{
get
{
if (_controlsProvider == null)
{
// Activate the WinUI controls' built-in metadata provider.
// This knows about all control types (TextCommandBarFlyout, etc.)
var provider = new Microsoft.UI.Xaml.XamlTypeInfo.XamlControlsXamlMetaDataProvider();
_controlsProvider = provider;
}
return _controlsProvider;
}
var t = global::System.Type.GetType("Microsoft.UI.Reactor.Reactor_XamlTypeInfo.XamlMetaDataProvider, Reactor", throwOnError: false);
return t is null
? new Microsoft.UI.Xaml.XamlTypeInfo.XamlControlsXamlMetaDataProvider()
: (IXamlMetadataProvider)global::System.Activator.CreateInstance(t)!;
}

// Fallback provider covering types WinUI may look up by-string that are not in the
// generated library provider (e.g. user-defined types in the consuming project
// referenced by ResourceDictionary keys). Additive safety net — in the normal path
// the Reactor provider already satisfies queries.
private IXamlMetadataProvider? _coreProvider;
private IXamlMetadataProvider CoreProvider => _coreProvider ??= new Hosting.ReactorCoreXamlMetaDataProvider();

/// <summary>
/// Optional callback for unhandled exceptions. If set, called before deciding whether to handle.
/// Return true to mark the exception as handled; return false (or leave null) to let it crash.
Expand All @@ -665,6 +682,13 @@ private IXamlMetadataProvider ControlsProvider

public ReactorApplication()
{
// Loads ReactorApplication.xaml (which references XamlControlsResources) via the
// XAML-compiled, XBF-deserialized path. Under native AOT, constructing
// XamlControlsResources programmatically crashes — putting it in an Application-level
// XAML and letting the XAML runtime activate it through LoadComponent during
// Application construction matches what App.xaml-based projects do and is AOT-safe.
InitializeComponent();

UnhandledException += (_, e) =>
{
_logger.LogError(e.Exception, "UnhandledException: {ExceptionType}: {ExceptionMessage}", e.Exception.GetType().Name, e.Exception.Message);
Expand All @@ -677,8 +701,6 @@ public ReactorApplication()

protected override void OnLaunched(LaunchActivatedEventArgs args)
{
// Load WinUI control theme resources programmatically.
Resources.MergedDictionaries.Add(new XamlControlsResources());

var opts = ReactorApp.Options;
var window = new Window { Title = opts.WindowTitle };
Expand All @@ -703,9 +725,14 @@ protected override void OnLaunched(LaunchActivatedEventArgs args)
window.Activate();
}

// IXamlMetadataProvider — delegates to the WinUI controls' built-in provider
// so custom control types can be resolved from XBF theme resources.
public IXamlType GetXamlType(Type type) => ControlsProvider.GetXamlType(type);
public IXamlType GetXamlType(string fullName) => ControlsProvider.GetXamlType(fullName);
public XmlnsDefinition[] GetXmlnsDefinitions() => ControlsProvider.GetXmlnsDefinitions();
// IXamlMetadataProvider — delegate to the library's generated provider (which already
// chains to XamlControlsXamlMetaDataProvider internally) and fall back to the core
// provider for any schema-only types the generated one doesn't carry. Returning null
// here is the WinUI convention for "unknown type" even though the WinRT interface
// types it as non-nullable.
public IXamlType GetXamlType(Type type)
=> (ReactorProvider.GetXamlType(type) ?? CoreProvider.GetXamlType(type))!;
public IXamlType GetXamlType(string fullName)
=> (ReactorProvider.GetXamlType(fullName) ?? CoreProvider.GetXamlType(fullName))!;
public XmlnsDefinition[] GetXmlnsDefinitions() => ReactorProvider.GetXmlnsDefinitions();
}
8 changes: 8 additions & 0 deletions src/Reactor/Hosting/ReactorApplication.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<Application
x:Class="Microsoft.UI.Reactor.ReactorApplication"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Application.Resources>
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
</Application.Resources>
</Application>
149 changes: 149 additions & 0 deletions src/Reactor/Hosting/ReactorCoreXamlMetaDataProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Markup;
using Microsoft.UI.Xaml.Media;

namespace Microsoft.UI.Reactor.Hosting;

/// <summary>
/// Hand-rolled IXamlMetadataProvider that covers system primitives, core Microsoft.UI.Xaml
/// types, common value structs, and the enums referenced by XamlControlsResources' internal
/// XAML. Needed under native AOT because the default XamlControlsXamlMetaDataProvider only
/// covers types in the Microsoft.UI.Xaml.Controls namespace. Under JIT, the XAML parser
/// falls back to reflective type resolution for unknown names; under AOT that path throws,
/// producing a STATUS_APPLICATION_INTERNAL_EXCEPTION crash inside Microsoft.UI.Xaml.dll
/// during Application bootstrap (when XamlControlsResources loads its theme dictionaries).
/// </summary>
internal sealed partial class ReactorCoreXamlMetaDataProvider : IXamlMetadataProvider
{
private static readonly (string Name, Type Type)[] s_entries =
[
// System primitives — WinUI queries these during Setter Value resolution and schema checks.
("Object", typeof(object)),
("Boolean", typeof(bool)),
("Byte", typeof(byte)),
("Int16", typeof(short)),
("Int32", typeof(int)),
("Int64", typeof(long)),
("Single", typeof(float)),
("Double", typeof(double)),
("Char", typeof(char)),
("String", typeof(string)),
("DateTime", typeof(DateTime)),
("TimeSpan", typeof(TimeSpan)),
("Guid", typeof(Guid)),
("Uri", typeof(Uri)),

// Core Microsoft.UI.Xaml — not in XamlControlsXamlMetaDataProvider because
// that one only covers Microsoft.UI.Xaml.Controls.*
("Microsoft.UI.Xaml.DependencyObject", typeof(DependencyObject)),
("Microsoft.UI.Xaml.UIElement", typeof(UIElement)),
("Microsoft.UI.Xaml.FrameworkElement", typeof(FrameworkElement)),
("Microsoft.UI.Xaml.ResourceDictionary", typeof(ResourceDictionary)),
("Microsoft.UI.Xaml.Style", typeof(Style)),
("Microsoft.UI.Xaml.Setter", typeof(Setter)),
("Microsoft.UI.Xaml.SetterBase", typeof(SetterBase)),
("Microsoft.UI.Xaml.DataTemplate", typeof(DataTemplate)),
("Microsoft.UI.Xaml.FrameworkTemplate", typeof(FrameworkTemplate)),

// Enums referenced by Setter values in theme dictionaries.
("Microsoft.UI.Xaml.Visibility", typeof(Visibility)),
("Microsoft.UI.Xaml.HorizontalAlignment", typeof(HorizontalAlignment)),
("Microsoft.UI.Xaml.VerticalAlignment", typeof(VerticalAlignment)),
("Microsoft.UI.Xaml.TextAlignment", typeof(TextAlignment)),
("Microsoft.UI.Xaml.TextWrapping", typeof(TextWrapping)),
("Microsoft.UI.Xaml.TextTrimming", typeof(TextTrimming)),
("Microsoft.UI.Xaml.FlowDirection", typeof(FlowDirection)),
("Microsoft.UI.Xaml.GridUnitType", typeof(GridUnitType)),
("Microsoft.UI.Xaml.Controls.Orientation",typeof(Orientation)),
("Microsoft.UI.Xaml.Controls.ControlTemplate", typeof(ControlTemplate)),

// Structs serialized in XAML attribute form.
("Microsoft.UI.Xaml.Thickness", typeof(Thickness)),
("Microsoft.UI.Xaml.CornerRadius", typeof(CornerRadius)),
("Microsoft.UI.Xaml.GridLength", typeof(GridLength)),
("Microsoft.UI.Xaml.Duration", typeof(Duration)),

// Media primitives.
("Microsoft.UI.Xaml.Media.Brush", typeof(Brush)),
("Microsoft.UI.Xaml.Media.SolidColorBrush", typeof(SolidColorBrush)),

// Windows namespace structs used in XAML.
("Windows.UI.Color", typeof(global::Windows.UI.Color)),
("Windows.Foundation.Size", typeof(global::Windows.Foundation.Size)),
("Windows.Foundation.Point", typeof(global::Windows.Foundation.Point)),
("Windows.Foundation.Rect", typeof(global::Windows.Foundation.Rect)),
];

private static readonly Dictionary<string, Type> s_byName = BuildNameMap();
private static readonly Dictionary<Type, string> s_byType = BuildTypeMap();

private static Dictionary<string, Type> BuildNameMap()
{
var map = new Dictionary<string, Type>(s_entries.Length, StringComparer.Ordinal);
foreach (var (name, type) in s_entries)
map[name] = type;
return map;
}

private static Dictionary<Type, string> BuildTypeMap()
{
var map = new Dictionary<Type, string>(s_entries.Length);
foreach (var (name, type) in s_entries)
map[type] = name;
return map;
}

public IXamlType? GetXamlType(Type type)
=> s_byType.TryGetValue(type, out var name) ? new CoreXamlType(name, type) : null;

public IXamlType? GetXamlType(string fullName)
=> s_byName.TryGetValue(fullName, out var type) ? new CoreXamlType(fullName, type) : null;

public XmlnsDefinition[] GetXmlnsDefinitions() => [];

/// <summary>
/// Minimal IXamlType that satisfies schema-level lookups. WinUI's XAML loader calls
/// GetXamlType during parsing to verify that types referenced in XAML exist; for system
/// and schema-only types, returning a non-null stub with correct FullName + UnderlyingType
/// is sufficient. Activation and member access are unreachable for these types because
/// Reactor apps do not construct them from XAML markup — they only appear as schema
/// references inside the WinUI theme dictionaries.
/// </summary>
private sealed partial class CoreXamlType : IXamlType
{
public CoreXamlType(string fullName, Type underlyingType)
{
FullName = fullName;
UnderlyingType = underlyingType;
}

public string FullName { get; }
public Type UnderlyingType { get; }
public IXamlType? BaseType => null;
public IXamlMember? ContentProperty => null;
public bool IsArray => false;
public bool IsCollection => false;
public bool IsConstructible => false;
public bool IsDictionary => false;
public bool IsMarkupExtension => false;
public bool IsBindable => false;
public bool IsReturnTypeStub => false;
public bool IsLocalType => false;
public IXamlType? ItemType => null;
public IXamlType? KeyType => null;
public IXamlType? BoxedType => null;
public IXamlMember? GetMember(string name) => null;
public object ActivateInstance() => throw new NotSupportedException($"{FullName} is schema-only; cannot activate from XAML.");
public void AddToMap(object instance, object key, object item) => throw new NotSupportedException();
public void AddToVector(object instance, object item) => throw new NotSupportedException();
public void RunInitializer() { }

public object CreateFromString(string input)
{
if (UnderlyingType.IsEnum)
return Enum.Parse(UnderlyingType, input, ignoreCase: true);
throw new NotSupportedException($"Cannot parse '{input}' for schema-only type {FullName}.");
}
}
}
33 changes: 18 additions & 15 deletions tests/stress_perf/benchmark_results.csv
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
App,Percent,Duration_s,Avg_FPS,Min_FPS,Max_FPS,Avg_Update_ms,Max_Update_ms,Avg_Memory_MB,Peak_Memory_MB
WPF.Direct,10,9.1,17.8,0.9,22.8,1.0,2.8,832.2,1027.0
WinUI.Direct,10,7.9,24.7,3.0,30.3,2.2,4.2,452.5,484.0
WinUI.Bound,10,0,0,0,0,0,0,0,0
WinUI.Duct,10,7.7,20.7,8.9,23.2,0.1,0.5,468.9,481.0
WinUI.DirectX,10,7.4,37.5,23.1,40.7,0.1,0.5,142.0,142.0
WPF.Direct,50,8.5,6.4,1.3,9.9,4.1,26.8,830.2,1078.0
WinUI.Direct,50,7.7,8.1,6.9,8.7,9.1,13.5,462.0,471.0
WinUI.Bound,50,0,0,0,0,0,0,0,0
WinUI.Duct,50,7.6,7.9,6.4,8.7,0.3,1.7,464.7,474.0
WinUI.DirectX,50,7.2,38.0,26.9,40.7,0.1,2.0,135.6,136.0
WPF.Direct,100,9.0,4.0,1.1,4.7,5.7,15.2,677.2,947.0
WinUI.Direct,100,7.7,5.3,4.9,5.9,15.6,20.9,471.6,490.0
WinUI.Bound,100,0,0,0,0,0,0,0,0
WinUI.Duct,100,7.7,5.6,5.3,5.9,0.3,1.9,465.8,476.0
WinUI.DirectX,100,7.2,37.8,29.2,39.8,0.2,2.1,135.8,136.0
WPF.Direct,10,9.1,21.7,1.0,28.9,0.9,2.7,856.2,1006.0
WinUI.Direct,10,8.2,21.6,1.5,25.8,2.4,6.6,452.3,475.0
WinUI.Bound,10,8.8,20.3,0.7,25.0,6.8,12.8,513.8,562.0
WinUI.Reactor,10,7.7,18.6,11.4,21.4,0.1,0.9,475.9,494.0
WinUI.ReactorGrid,10,7.4,14.4,12.1,15.1,0.1,0.6,409.9,422.0
WinUI.DirectX,10,7.4,32.4,19.5,36.7,0.1,0.7,141.5,141.0
WPF.Direct,50,8.8,6.4,1.1,9.8,4.2,11.9,812.7,1051.0
WinUI.Direct,50,7.9,7.4,4.8,8.6,9.7,14.3,462.7,479.0
WinUI.Bound,50,8.8,6.5,0.7,7.5,27.7,54.8,513.1,562.0
WinUI.Reactor,50,7.8,8.2,7.2,10.3,0.3,1.5,475.8,497.0
WinUI.ReactorGrid,50,7.2,9.5,9.0,9.8,0.3,2.2,394.3,409.0
WinUI.DirectX,50,7.3,32.2,24.8,36.1,0.2,1.8,136.2,137.0
WPF.Direct,100,8.9,3.8,1.1,4.3,6.0,15.0,670.2,942.0
WinUI.Direct,100,7.7,5.1,4.4,5.5,16.4,24.3,460.7,473.0
WinUI.Bound,100,8.7,4.3,0.8,5.4,44.1,58.3,506.4,555.0
WinUI.Reactor,100,7.9,5.9,5.2,8.8,0.5,1.6,486.8,508.0
WinUI.ReactorGrid,100,7.5,7.7,6.8,8.5,0.4,1.8,401.6,416.0
WinUI.DirectX,100,7.3,32.4,21.9,34.8,0.2,3.2,135.9,137.0
19 changes: 19 additions & 0 deletions tests/stress_perf/benchmark_results_aot_publish.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
App,Percent,Duration_s,Avg_FPS,Min_FPS,Max_FPS,Avg_Update_ms,Max_Update_ms,Avg_Memory_MB,Peak_Memory_MB
WPF.Direct,10,9.7,19.0,0.7,27.6,1.0,7.1,816.7,992.0
WinUI.Direct,10,7.8,20.6,7.8,23.2,3.0,19.6,418.4,427.0
WinUI.Bound,10,8.9,18.8,0.6,23.1,7.5,10.8,494.3,532.0
WinUI.Reactor,10,7.8,18.5,13.7,20.7,0.1,0.1,436.1,446.0
WinUI.ReactorGrid,10,7.3,12.6,10.8,14.7,0.1,0.1,375.3,383.0
WinUI.DirectX,10,7.3,33.8,29.4,35.5,0.0,0.3,95.4,96.0
WPF.Direct,50,9.0,5.9,1.0,8.7,4.7,18.9,798.3,1056.0
WinUI.Direct,50,7.9,7.3,6.9,7.6,10.3,15.9,433.2,448.0
WinUI.Bound,50,8.9,5.7,0.6,7.7,34.9,64.9,497.0,548.0
WinUI.Reactor,50,7.7,8.0,6.6,13.0,0.1,0.2,451.9,458.0
WinUI.ReactorGrid,50,7.2,8.8,7.7,11.6,0.1,0.2,364.1,378.0
WinUI.DirectX,50,7.3,36.6,31.5,39.5,0.1,0.2,97.2,97.0
WPF.Direct,100,9.1,3.7,1.0,4.3,6.5,15.5,629.5,931.0
WinUI.Direct,100,7.8,5.0,4.2,6.6,19.4,27.6,427.9,440.0
WinUI.Bound,100,9.0,4.4,0.7,6.1,49.0,69.2,500.7,550.0
WinUI.Reactor,100,8.0,5.2,3.7,10.3,0.1,0.3,446.1,465.0
WinUI.ReactorGrid,100,7.4,7.0,5.9,8.7,0.1,0.2,361.2,375.0
WinUI.DirectX,100,7.3,24.9,20.4,29.7,0.1,0.3,95.0,95.0
77 changes: 77 additions & 0 deletions tests/stress_perf/run_bench_aot_publish.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
#!/bin/bash
# StressPerf benchmark against published outputs (AOT-compiled for supported variants).
# Runs 10/50/100% update rates, 7s each.
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"

DURATION=7
OUTFILE="$SCRIPT_DIR/benchmark_results_aot_publish.csv"

CONFIG="Release"
TFM_WINUI="net9.0-windows10.0.22621.0"
TFM_WPF="net9.0-windows"
PLATFORM="ARM64"
RID="win-arm64"

STRESS_DIR="$REPO_ROOT/tests/stress_perf"
DIRECT_EXE="$STRESS_DIR/StressPerf.Direct/bin/$PLATFORM/$CONFIG/$TFM_WINUI/$RID/publish/StressPerf.Direct.exe"
BOUND_EXE="$STRESS_DIR/StressPerf.Bound/bin/$PLATFORM/$CONFIG/$TFM_WINUI/$RID/publish/StressPerf.Bound.exe"
REACTOR_EXE="$STRESS_DIR/StressPerf.Reactor/bin/$PLATFORM/$CONFIG/$TFM_WINUI/$RID/publish/StressPerf.Reactor.exe"
REACTORGRID_EXE="$STRESS_DIR/StressPerf.ReactorGrid/bin/$PLATFORM/$CONFIG/$TFM_WINUI/$RID/publish/StressPerf.ReactorGrid.exe"
WPF_EXE="$STRESS_DIR/StressPerf.Wpf/bin/$PLATFORM/$CONFIG/$TFM_WPF/$RID/publish/StressPerf.Wpf.exe"
DIRECTX_EXE="$STRESS_DIR/StressPerf.DirectX/bin/$PLATFORM/$CONFIG/$TFM_WINUI/$RID/publish/StressPerf.DirectX.exe"

echo "App,Percent,Duration_s,Avg_FPS,Min_FPS,Max_FPS,Avg_Update_ms,Max_Update_ms,Avg_Memory_MB,Peak_Memory_MB" > "$OUTFILE"

parse_report() {
local file="$1" app="$2" pct="$3"
if [ ! -f "$file" ]; then
echo "$app,$pct,0,0,0,0,0,0,0,0" >> "$OUTFILE"
return
fi
local duration=$(grep "Duration:" "$file" | awk '{print $NF}' | tr -d 's')
local avg_fps=$(grep "Avg FPS:" "$file" | awk '{print $NF}')
local min_fps=$(grep "Min FPS:" "$file" | awk '{print $NF}')
local max_fps=$(grep "Max FPS:" "$file" | awk '{print $NF}')
local avg_update=$(grep "Avg Update:" "$file" | awk '{print $(NF-1)}')
local max_update=$(grep "Max Update:" "$file" | awk '{print $(NF-1)}')
local avg_mem=$(grep "Avg Memory:" "$file" | awk '{print $(NF-1)}')
local peak_mem=$(grep "Peak Memory:" "$file" | awk '{print $(NF-1)}')
echo "$app,$pct,$duration,$avg_fps,$min_fps,$max_fps,$avg_update,$max_update,$avg_mem,$peak_mem" >> "$OUTFILE"
}

run_app() {
local exe="$1" name="$2" pct="$3"
local exe_dir
exe_dir=$(dirname "$exe")
if [ ! -f "$exe" ]; then
echo " SKIP $name (not built: $exe)"
echo "$name,$pct,0,0,0,0,0,0,0,0" >> "$OUTFILE"
return
fi
find "$exe_dir" -maxdepth 1 -iname "*.report.txt" -delete 2>/dev/null || true
echo " Running $name @ ${pct}%..."
"$exe" --headless --percent "$pct" --duration "$DURATION" 2>/dev/null || true
local report_file
report_file=$(find "$exe_dir" -maxdepth 1 -iname "*.report.txt" -type f 2>/dev/null | head -1)
[ -z "$report_file" ] && report_file=$(find "$exe_dir/.." -iname "*.report.txt" -type f 2>/dev/null | head -1)
parse_report "$report_file" "$name" "$pct"
}

echo "=== StressPerf (AOT publish, 10/50/100%) ==="
echo "Duration per run: ${DURATION}s"
echo ""
for pct in 10 50 100; do
echo "--- ${pct}% update rate ---"
run_app "$WPF_EXE" "WPF.Direct" "$pct"
run_app "$DIRECT_EXE" "WinUI.Direct" "$pct"
run_app "$BOUND_EXE" "WinUI.Bound" "$pct"
run_app "$REACTOR_EXE" "WinUI.Reactor" "$pct"
run_app "$REACTORGRID_EXE" "WinUI.ReactorGrid" "$pct"
run_app "$DIRECTX_EXE" "WinUI.DirectX" "$pct"
echo ""
done

echo "=== Done ==="
cat "$OUTFILE"
Loading