From 98e0e12dff7c6050d358df3706cd71ac91198eb2 Mon Sep 17 00:00:00 2001 From: Chris Anderson Date: Wed, 22 Apr 2026 17:46:47 -0700 Subject: [PATCH] Fix native AOT crash in ReactorApplication at XAML bootstrap MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reactor-based WinUI apps published with PublishAot=true crashed at Application startup with STATUS_APPLICATION_INTERNAL_EXCEPTION (0xc000027b) inside Microsoft.UI.Xaml.dll. The crash happened when OnLaunched ran `Resources.MergedDictionaries.Add(new XamlControlsResources())` — under AOT the runtime XAML parse path that programmatic construction triggers is not safe, whereas the same type loaded via an App.xaml-compiled XBF (as App.xaml-based projects do) is safe. Fix routes XamlControlsResources through the XAML-compiled path by adding ReactorApplication.xaml as a library-level App-style companion and calling InitializeComponent() in the ReactorApplication constructor, matching the order and code path used by App.xaml-based consumers. GetXamlType now delegates to the XAML-compiler-generated Reactor_XamlTypeInfo.XamlMetaDataProvider (late-bound via Type.GetType with a DynamicDependency to survive trimming) with a hand-rolled ReactorCoreXamlMetaDataProvider as a fallback for primitives, core Microsoft.UI.Xaml types, enums and structs that the Controls-only provider does not carry. Benchmark (ARM64, 7s per cell; previously both Reactor variants crashed under AOT): WinUI.Reactor runs 18.5/8.0/5.2 FPS at 10/50/100% updates; WinUI.ReactorGrid runs 12.6/8.8/7.0 FPS. Both show ~0.1 ms average update cost — the lowest of any WinUI variant. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/Reactor/Hosting/ReactorApp.cs | 69 +++++--- src/Reactor/Hosting/ReactorApplication.xaml | 8 + .../ReactorCoreXamlMetaDataProvider.cs | 149 ++++++++++++++++++ tests/stress_perf/benchmark_results.csv | 33 ++-- .../benchmark_results_aot_publish.csv | 19 +++ tests/stress_perf/run_bench_aot_publish.sh | 77 +++++++++ 6 files changed, 319 insertions(+), 36 deletions(-) create mode 100644 src/Reactor/Hosting/ReactorApplication.xaml create mode 100644 src/Reactor/Hosting/ReactorCoreXamlMetaDataProvider.cs create mode 100644 tests/stress_perf/benchmark_results_aot_publish.csv create mode 100644 tests/stress_perf/run_bench_aot_publish.sh diff --git a/src/Reactor/Hosting/ReactorApp.cs b/src/Reactor/Hosting/ReactorApp.cs index 681148c7f..566550816 100644 --- a/src/Reactor/Hosting/ReactorApp.cs +++ b/src/Reactor/Hosting/ReactorApp.cs @@ -638,23 +638,40 @@ private static void RunOnSta(Action action) /// 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(); + /// /// 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. @@ -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); @@ -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 }; @@ -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(); } diff --git a/src/Reactor/Hosting/ReactorApplication.xaml b/src/Reactor/Hosting/ReactorApplication.xaml new file mode 100644 index 000000000..b6acb8301 --- /dev/null +++ b/src/Reactor/Hosting/ReactorApplication.xaml @@ -0,0 +1,8 @@ + + + + + diff --git a/src/Reactor/Hosting/ReactorCoreXamlMetaDataProvider.cs b/src/Reactor/Hosting/ReactorCoreXamlMetaDataProvider.cs new file mode 100644 index 000000000..73ae5a447 --- /dev/null +++ b/src/Reactor/Hosting/ReactorCoreXamlMetaDataProvider.cs @@ -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; + +/// +/// 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). +/// +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 s_byName = BuildNameMap(); + private static readonly Dictionary s_byType = BuildTypeMap(); + + private static Dictionary BuildNameMap() + { + var map = new Dictionary(s_entries.Length, StringComparer.Ordinal); + foreach (var (name, type) in s_entries) + map[name] = type; + return map; + } + + private static Dictionary BuildTypeMap() + { + var map = new Dictionary(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() => []; + + /// + /// 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. + /// + 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}."); + } + } +} diff --git a/tests/stress_perf/benchmark_results.csv b/tests/stress_perf/benchmark_results.csv index b29366fa4..40473ef34 100644 --- a/tests/stress_perf/benchmark_results.csv +++ b/tests/stress_perf/benchmark_results.csv @@ -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 diff --git a/tests/stress_perf/benchmark_results_aot_publish.csv b/tests/stress_perf/benchmark_results_aot_publish.csv new file mode 100644 index 000000000..72b1e6f1e --- /dev/null +++ b/tests/stress_perf/benchmark_results_aot_publish.csv @@ -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 diff --git a/tests/stress_perf/run_bench_aot_publish.sh b/tests/stress_perf/run_bench_aot_publish.sh new file mode 100644 index 000000000..00f68573a --- /dev/null +++ b/tests/stress_perf/run_bench_aot_publish.sh @@ -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"