From 18cf1190077e13a3d718f3d2c232a8719e0a79c4 Mon Sep 17 00:00:00 2001 From: David Date: Thu, 6 Jun 2024 09:19:20 -0400 Subject: [PATCH] feat: Improve HR diag view --- ...ServerHotReloadProcessor.MetadataUpdate.cs | 10 +- .../HotReload/ServerHotReloadProcessor.cs | 67 +++--- .../ClientHotReloadProcessor.Agent.cs | 16 +- .../ClientHotReloadProcessor.Common.Status.cs | 202 ++++++++++++++++++ ...ClientHotReloadProcessor.MetadataUpdate.cs | 77 ++++--- .../HotReload/ClientHotReloadProcessor.cs | 13 +- .../HotReload/HotReloadStatusView.cs | 201 +++++++++++++---- .../HotReload/HotReloadStatusView.xaml | 45 +++- ...loadResult.cs => HotReloadServerResult.cs} | 4 +- .../Messages/HotReloadStatusMessage.cs | 9 +- .../MetadataUpdater/ElementUpdaterAgent.cs | 5 +- .../RemoteControlClient.Diagnostics.cs | 2 +- .../Diagnostics/DiagnosticView.Factories.cs | 4 +- src/Uno.UI/Helpers/TypeMappings.cs | 9 +- 14 files changed, 520 insertions(+), 144 deletions(-) create mode 100644 src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.Common.Status.cs rename src/Uno.UI.RemoteControl/HotReload/Messages/{HotReloadResult.cs => HotReloadServerResult.cs} (89%) diff --git a/src/Uno.UI.RemoteControl.Server.Processors/HotReload/ServerHotReloadProcessor.MetadataUpdate.cs b/src/Uno.UI.RemoteControl.Server.Processors/HotReload/ServerHotReloadProcessor.MetadataUpdate.cs index 213757f40f49..07318b189bee 100644 --- a/src/Uno.UI.RemoteControl.Server.Processors/HotReload/ServerHotReloadProcessor.MetadataUpdate.cs +++ b/src/Uno.UI.RemoteControl.Server.Processors/HotReload/ServerHotReloadProcessor.MetadataUpdate.cs @@ -164,7 +164,7 @@ private async Task ProcessMetadataChanges(IEnumerable filePaths) } } - private async Task ProcessSolutionChanged(HotReloadOperation hotReload, string file, CancellationToken cancellationToken) + private async Task ProcessSolutionChanged(HotReloadServerOperation hotReload, string file, CancellationToken cancellationToken) { if (!await EnsureSolutionInitializedAsync() || _currentSolution is null || _hotReloadService is null) { @@ -219,12 +219,12 @@ private async Task ProcessSolutionChanged(HotReloadOperation hotReload, st if (diagnostics.IsDefaultOrEmpty) { await UpdateMetadata(file, updates); - hotReload.NotifyIntermediate(file, HotReloadResult.NoChanges); + hotReload.NotifyIntermediate(file, HotReloadServerResult.NoChanges); } else { _reporter.Output($"Got {diagnostics.Length} errors"); - hotReload.NotifyIntermediate(file, HotReloadResult.Failed); + hotReload.NotifyIntermediate(file, HotReloadServerResult.Failed); } // HotReloadEventSource.Log.HotReloadEnd(HotReloadEventSource.StartType.CompilationHandler); @@ -241,7 +241,7 @@ private async Task ProcessSolutionChanged(HotReloadOperation hotReload, st _reporter.Verbose(CSharpDiagnosticFormatter.Instance.Format(diagnostic, CultureInfo.InvariantCulture)); } - hotReload.NotifyIntermediate(file, HotReloadResult.RudeEdit); + hotReload.NotifyIntermediate(file, HotReloadServerResult.RudeEdit); // HotReloadEventSource.Log.HotReloadEnd(HotReloadEventSource.StartType.CompilationHandler); return false; @@ -252,7 +252,7 @@ private async Task ProcessSolutionChanged(HotReloadOperation hotReload, st sw.Stop(); await UpdateMetadata(file, updates); - hotReload.NotifyIntermediate(file, HotReloadResult.Success); + hotReload.NotifyIntermediate(file, HotReloadServerResult.Success); // HotReloadEventSource.Log.HotReloadEnd(HotReloadEventSource.StartType.CompilationHandler); return true; diff --git a/src/Uno.UI.RemoteControl.Server.Processors/HotReload/ServerHotReloadProcessor.cs b/src/Uno.UI.RemoteControl.Server.Processors/HotReload/ServerHotReloadProcessor.cs index 32c9ba00655e..86306d4cd6d1 100644 --- a/src/Uno.UI.RemoteControl.Server.Processors/HotReload/ServerHotReloadProcessor.cs +++ b/src/Uno.UI.RemoteControl.Server.Processors/HotReload/ServerHotReloadProcessor.cs @@ -73,7 +73,7 @@ public async Task ProcessIdeMessage(IdeMessage message, CancellationToken ct) #region Hot-relaod state private HotReloadState _globalState; // This actually contains only the initializing stat (i.e. Disabled, Initializing, Idle). Processing state is _current != null. - private HotReloadOperation? _current; // I.e. head of the operation chain list + private HotReloadServerOperation? _current; // I.e. head of the operation chain list public enum HotReloadEventSource { @@ -89,13 +89,13 @@ private async ValueTask EnsureHotReloadStarted() } } - private async ValueTask StartHotReload(ImmutableHashSet? filesPaths) + private async ValueTask StartHotReload(ImmutableHashSet? filesPaths) { var previous = _current; - HotReloadOperation? current, @new; + HotReloadServerOperation? current, @new; while (true) { - @new = new HotReloadOperation(this, previous, filesPaths); + @new = new HotReloadServerOperation(this, previous, filesPaths); current = Interlocked.CompareExchange(ref _current, @new, previous); if (current == previous) { @@ -113,13 +113,13 @@ private async ValueTask StartHotReload(ImmutableHashSet StartOrContinueHotReload(ImmutableHashSet? filesPaths = null) + private async ValueTask StartOrContinueHotReload(ImmutableHashSet? filesPaths = null) => _current is { } current && (filesPaths is null || current.TryMerge(filesPaths)) ? current : await StartHotReload(filesPaths); private ValueTask AbortHotReload() - => _current?.Complete(HotReloadResult.Aborted) ?? SendUpdate(); + => _current?.Complete(HotReloadServerResult.Aborted) ?? SendUpdate(); private async ValueTask Notify(HotReloadEvent evt, HotReloadEventSource source = HotReloadEventSource.DevServer) { @@ -147,31 +147,31 @@ private async ValueTask Notify(HotReloadEvent evt, HotReloadEventSource source = break; case HotReloadEvent.Completed: - await (await StartOrContinueHotReload()).DeferComplete(HotReloadResult.Success); + await (await StartOrContinueHotReload()).DeferComplete(HotReloadServerResult.Success); break; case HotReloadEvent.NoChanges: - await (await StartOrContinueHotReload()).Complete(HotReloadResult.NoChanges); + await (await StartOrContinueHotReload()).Complete(HotReloadServerResult.NoChanges); break; case HotReloadEvent.Failed: - await (await StartOrContinueHotReload()).Complete(HotReloadResult.Failed); + await (await StartOrContinueHotReload()).Complete(HotReloadServerResult.Failed); break; case HotReloadEvent.RudeEdit: case HotReloadEvent.RudeEditDialogButton: - await (await StartOrContinueHotReload()).Complete(HotReloadResult.RudeEdit); + await (await StartOrContinueHotReload()).Complete(HotReloadServerResult.RudeEdit); break; } } - private async ValueTask SendUpdate(HotReloadOperation? completing = null) + private async ValueTask SendUpdate(HotReloadServerOperation? completing = null) { var state = _globalState; - var operations = ImmutableList.Empty; + var operations = ImmutableList.Empty; if (state is not HotReloadState.Disabled && (_current ?? completing) is { } current) { - var infos = ImmutableList.CreateBuilder(); + var infos = ImmutableList.CreateBuilder(); var foundCompleting = completing is null; LoadInfos(current); if (!foundCompleting) @@ -181,7 +181,7 @@ private async ValueTask SendUpdate(HotReloadOperation? completing = null) operations = infos.ToImmutable(); - void LoadInfos(HotReloadOperation? operation) + void LoadInfos(HotReloadServerOperation? operation) { while (operation is not null) { @@ -191,7 +191,7 @@ void LoadInfos(HotReloadOperation? operation) } foundCompleting |= operation == completing; - infos.Add(new(operation.Id, operation.FilePaths, operation.Result)); + infos.Add(new(operation.Id, operation.StartTime, operation.FilePaths, operation.CompletionTime, operation.Result)); operation = operation.Previous!; } } @@ -203,7 +203,7 @@ void LoadInfos(HotReloadOperation? operation) /// /// A hot-reload operation that is in progress. /// - private class HotReloadOperation + private class HotReloadServerOperation { // Delay to wait without any update to consider operation was aborted. private static readonly TimeSpan _timeoutDelay = TimeSpan.FromSeconds(30); @@ -212,7 +212,7 @@ private class HotReloadOperation private static long _count; private readonly ServerHotReloadProcessor _owner; - private readonly HotReloadOperation? _previous; + private readonly HotReloadServerOperation? _previous; private readonly Timer _timeout; private ImmutableHashSet _filePaths; @@ -221,21 +221,25 @@ private class HotReloadOperation public long Id { get; } = Interlocked.Increment(ref _count); - public HotReloadOperation? Previous => _previous; + public DateTimeOffset StartTime { get; } = DateTimeOffset.Now; + + public DateTimeOffset? CompletionTime { get; private set; } + + public HotReloadServerOperation? Previous => _previous; public ImmutableHashSet FilePaths => _filePaths; - public HotReloadResult? Result => _result is -1 ? null : (HotReloadResult)_result; + public HotReloadServerResult? Result => _result is -1 ? null : (HotReloadServerResult)_result; /// The previous hot-reload operation which has to be considered as aborted when this new one completes. - public HotReloadOperation(ServerHotReloadProcessor owner, HotReloadOperation? previous, ImmutableHashSet? filePaths = null) + public HotReloadServerOperation(ServerHotReloadProcessor owner, HotReloadServerOperation? previous, ImmutableHashSet? filePaths = null) { _owner = owner; _previous = previous; _filePaths = filePaths ?? _empty; _timeout = new Timer( - static that => _ = ((HotReloadOperation)that!).Complete(HotReloadResult.Aborted), + static that => _ = ((HotReloadServerOperation)that!).Complete(HotReloadServerResult.Aborted), this, _timeoutDelay, Timeout.InfiniteTimeSpan); @@ -286,9 +290,9 @@ public bool TryMerge(ImmutableHashSet filePaths) } // Note: This is a patch until the dev-server based hot-reload treat files per batch instead of file per file. - private HotReloadResult _aggregatedResult = HotReloadResult.NoChanges; + private HotReloadServerResult _aggregatedResult = HotReloadServerResult.NoChanges; private int _aggregatedFilesCount; - public void NotifyIntermediate(string file, HotReloadResult result) + public void NotifyIntermediate(string file, HotReloadServerResult result) { if (Interlocked.Increment(ref _aggregatedFilesCount) is 1) { @@ -296,7 +300,7 @@ public void NotifyIntermediate(string file, HotReloadResult result) return; } - _aggregatedResult = (HotReloadResult)Math.Max((int)_aggregatedResult, (int)result); + _aggregatedResult = (HotReloadServerResult)Math.Max((int)_aggregatedResult, (int)result); _timeout.Change(_timeoutDelay, Timeout.InfiniteTimeSpan); } @@ -309,9 +313,9 @@ public async ValueTask CompleteUsingIntermediates() /// /// As errors might get a bit after the complete from the IDE, we can defer the completion of the operation. /// - public async ValueTask DeferComplete(HotReloadResult result, Exception? exception = null) + public async ValueTask DeferComplete(HotReloadServerResult result, Exception? exception = null) { - Debug.Assert(result != HotReloadResult.InternalError || exception is not null); // For internal error we should always provide an exception! + Debug.Assert(result != HotReloadServerResult.InternalError || exception is not null); // For internal error we should always provide an exception! if (Interlocked.CompareExchange(ref _deferredCompletion, new CancellationTokenSource(), null) is null) { @@ -324,12 +328,12 @@ public async ValueTask DeferComplete(HotReloadResult result, Exception? exceptio } } - public ValueTask Complete(HotReloadResult result, Exception? exception = null) + public ValueTask Complete(HotReloadServerResult result, Exception? exception = null) => Complete(result, exception, isFromNext: false); - private async ValueTask Complete(HotReloadResult result, Exception? exception, bool isFromNext) + private async ValueTask Complete(HotReloadServerResult result, Exception? exception, bool isFromNext) { - Debug.Assert(result != HotReloadResult.InternalError || exception is not null); // For internal error we should always provide an exception! + Debug.Assert(result != HotReloadServerResult.InternalError || exception is not null); // For internal error we should always provide an exception! // Remove this from current Interlocked.CompareExchange(ref _owner._current, null, this); @@ -341,13 +345,14 @@ private async ValueTask Complete(HotReloadResult result, Exception? exception, b return; // Already completed } + CompletionTime = DateTimeOffset.Now; await _timeout.DisposeAsync(); // Consider previous hot-reload operation(s) as aborted (this is actually a chain list) if (_previous is not null) { await _previous.Complete( - HotReloadResult.Aborted, + HotReloadServerResult.Aborted, new TimeoutException("An more recent hot-reload operation has completed."), isFromNext: true); } @@ -463,7 +468,7 @@ private async Task ProcessUpdateFile(UpdateFile message) } catch (Exception ex) { - await hotReload.Complete(HotReloadResult.InternalError, ex); + await hotReload.Complete(HotReloadServerResult.InternalError, ex); await _remoteControlServer.SendFrame(new UpdateFileResponse(message.RequestId, message.FilePath, FileUpdateResult.Failed, ex.Message)); } diff --git a/src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.Agent.cs b/src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.Agent.cs index 71d069a67f1c..2039cb05b1b4 100644 --- a/src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.Agent.cs +++ b/src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.Agent.cs @@ -142,17 +142,7 @@ private string[] GetMetadataUpdateCapabilities() return Array.Empty(); } - private enum HotReloadSource - { - Runtime, - DevServer, - Manual - } -#pragma warning disable CS0414 // Field is assigned but its value is never used - private static HotReloadSource _source; -#pragma warning restore CS0414 // Field is assigned but its value is never used - - private void AssemblyReload(AssemblyDeltaReload assemblyDeltaReload) + private void ProcessAssemblyReload(AssemblyDeltaReload assemblyDeltaReload) { try { @@ -185,7 +175,7 @@ private void AssemblyReload(AssemblyDeltaReload assemblyDeltaReload) UpdatedTypes = ReadIntArray(changedTypesReader) }; - _source = HotReloadSource.DevServer; + _status.ConfigureSourceForNextOperation(HotReloadSource.DevServer); _agent?.ApplyDeltas(new[] { delta }); if (this.Log().IsEnabled(LogLevel.Trace)) @@ -210,7 +200,7 @@ private void AssemblyReload(AssemblyDeltaReload assemblyDeltaReload) } finally { - _source = default; // runtime + _status.ConfigureSourceForNextOperation(default); // runtime } } diff --git a/src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.Common.Status.cs b/src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.Common.Status.cs new file mode 100644 index 000000000000..e55e46bb3416 --- /dev/null +++ b/src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.Common.Status.cs @@ -0,0 +1,202 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Linq; +using System.Reflection; +using System.Threading; +using Uno.Diagnostics.UI; +using Uno.UI.RemoteControl.HotReload.Messages; + +namespace Uno.UI.RemoteControl.HotReload; + +public partial class ClientHotReloadProcessor +{ + private readonly StatusSink _status = new(); + + internal enum HotReloadSource + { + Runtime, + DevServer, + Manual + } + + internal enum HotReloadClientResult + { + /// + /// Successful hot-reload. + /// + Success = 1, + + /// + /// Changes cannot be applied in local app due to handler errors. + /// + Failed = 2, + + /// + /// The changes have been ignored. + /// + Ignored = 256, + } + + internal record Status( + HotReloadState State, + (HotReloadState State, IImmutableList Operations) Server, + (HotReloadState State, IImmutableList Operations) Local); + + private class StatusSink + { + private readonly DiagnosticView _view = DiagnosticView.Register("Hot reload", (view, status) => view.Update(status)); + + private HotReloadState? _serverState; + private ImmutableDictionary _serverOperations = ImmutableDictionary.Empty; + private ImmutableList _localOperations = ImmutableList.Empty; + private HotReloadSource _source; + + public void ReportServerStatus(HotReloadStatusMessage status) + { + _serverState = status.State; + ImmutableInterlocked.Update(ref _serverOperations, UpdateOperations, status.Operations); + NotifyStatusChanged(); + + static ImmutableDictionary UpdateOperations(ImmutableDictionary history, IImmutableList udpated) + { + var updatedHistory = history.ToBuilder(); + foreach (var op in udpated) + { + updatedHistory[op.Id] = op; + } + + return updatedHistory.ToImmutable(); + } + } + + public void ConfigureSourceForNextOperation(HotReloadSource source) + => _source = source; + + public HotReloadClientOperation ReportLocalStarting(Type[] types) + { + var op = new HotReloadClientOperation(_source, types, NotifyStatusChanged); + ImmutableInterlocked.Update(ref _localOperations, static (history, op) => history.Add(op).Sort(Compare), op); + NotifyStatusChanged(); + + return op; + + static int Compare(HotReloadClientOperation left, HotReloadClientOperation right) + => Comparer.Default.Compare(left.Id, right.Id); + } + + private void NotifyStatusChanged() + => _view.Update(GetStatus()); + + private Status GetStatus() + { + var serverState = _serverState ?? (_localOperations.Any() ? HotReloadState.Idle /* no info */ : HotReloadState.Initializing); + var localState = _localOperations.Any(op => op.Result is null) ? HotReloadState.Processing : HotReloadState.Idle; + var globalState = _serverState is HotReloadState.Disabled ? HotReloadState.Disabled : (HotReloadState)Math.Max((int)serverState, (int)localState); + + return new(globalState, (serverState, _serverOperations.Values.ToImmutableArray()), (localState, _localOperations)); + } + } + + internal class HotReloadClientOperation + { + #region Current + [ThreadStatic] + private static HotReloadClientOperation? _opForCurrentUiThread; + + public static HotReloadClientOperation? GetForCurrentThread() + => _opForCurrentUiThread; + + public void SetCurrent() + { + Debug.Assert(_opForCurrentUiThread == null, "Only one operation should be active at once for a given UI thread."); + _opForCurrentUiThread = this; + } + + public void ResignCurrent() + { + Debug.Assert(_opForCurrentUiThread == this, "Another operation has been started for teh current UI thread."); + _opForCurrentUiThread = null; + } + #endregion + + private static int _count; + + private readonly Action _onUpdated; + private string[]? _curatedTypes; + private ImmutableList _exceptions = ImmutableList.Empty; + private int _result = -1; + + public HotReloadClientOperation(HotReloadSource source, Type[] types, Action onUpdated) + { + Source = source; + Types = types; + + _onUpdated = onUpdated; + } + + public int Id { get; } = Interlocked.Increment(ref _count); + + public DateTimeOffset StartTime { get; } = DateTimeOffset.Now; + + public HotReloadSource Source { get; } + + public Type[] Types { get; } + + public string[] CuratedTypes => _curatedTypes ??= Types + .Select(t => + { + var name = t.Name; + var versionIndex = t.Name.IndexOf('#'); + return versionIndex < 0 + ? default! + : $"{name[..versionIndex]} (v{name[(versionIndex + 1)..]})"; + }) + .Where(t => t is not null) + .ToArray(); + + public DateTimeOffset? EndTime { get; private set; } + + public HotReloadClientResult? Result => _result is -1 ? null : (HotReloadClientResult)_result; + + public ImmutableList Exceptions => _exceptions; + + public string? IgnoreReason { get; private set; } + + public void ReportError(MethodInfo source, Exception error) + => ReportError(error); // For now we just ignore the source + + public void ReportError(Exception error) + { + ImmutableInterlocked.Update(ref _exceptions, static (errors, error) => errors.Add(error), error); + _onUpdated(); + } + + public void ReportCompleted() + { + var result = (_exceptions, AbortReason: IgnoreReason) switch + { + ({ Count: > 0 }, _) => HotReloadClientResult.Failed, + (_, not null) => HotReloadClientResult.Ignored, + _ => HotReloadClientResult.Success + }; + + if (Interlocked.CompareExchange(ref _result, (int)result, -1) is -1) + { + EndTime = DateTimeOffset.Now; + _onUpdated(); + } + else + { + Debug.Fail("The result should not have already been set."); + } + } + + public void ReportIgnored(string reason) + { + IgnoreReason = reason; + ReportCompleted(); + } + } +} diff --git a/src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.MetadataUpdate.cs b/src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.MetadataUpdate.cs index a4e86740a634..71ffb50f539b 100644 --- a/src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.MetadataUpdate.cs +++ b/src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.MetadataUpdate.cs @@ -15,6 +15,7 @@ using Uno.UI.Helpers; using Uno.UI.RemoteControl.HotReload.MetadataUpdater; using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Automation.Text; using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Input; using Microsoft.UI.Xaml.Media; @@ -26,47 +27,43 @@ namespace Uno.UI.RemoteControl.HotReload; partial class ClientHotReloadProcessor { - private static int _isReloading; + private static int _isWaitingForTypeMapping; private static ElementUpdateAgent? _elementAgent; - private static Logger _log = typeof(ClientHotReloadProcessor).Log(); + private static readonly Logger _log = typeof(ClientHotReloadProcessor).Log(); private static Window? _currentWindow; private static ElementUpdateAgent ElementAgent { get { - _elementAgent ??= new ElementUpdateAgent(s => - { - if (_log.IsEnabled(LogLevel.Trace)) - { - _log.Trace(s); - } - }); + var log = _log.IsEnabled(LogLevel.Trace) + ? new Action(_log.Trace) + : static _ => { }; + + _elementAgent ??= new ElementUpdateAgent(log, static (callback, error) => HotReloadClientOperation.GetForCurrentThread()?.ReportError(callback, error)); return _elementAgent; } } - private static async Task ShouldReload() + private static async Task<(bool value, string reason)> ShouldReload() { - if (Interlocked.CompareExchange(ref _isReloading, 1, 0) == 1) + if (Interlocked.CompareExchange(ref _isWaitingForTypeMapping, 1, 0) == 1) { - return false; + return (false, "another reload is already waiting for type mapping to resume"); } try { - var waiter = TypeMappings.WaitForResume(); - if (!waiter.IsCompleted) - { - return false; - } - return await waiter; + var shouldReload = await TypeMappings.WaitForResume(); + return shouldReload + ? (true, string.Empty) + : (false, "type mapping prevent reload"); } finally { - Interlocked.Exchange(ref _isReloading, 0); + Interlocked.Exchange(ref _isWaitingForTypeMapping, 0); } } @@ -98,17 +95,22 @@ private static void ShowDiagnosticsOnFirstActivation(object snd, WindowActivated } } - private static async Task ReloadWithUpdatedTypes(Type[] updatedTypes) + /// + /// Run on UI thread to reload the visual tree with updated types + /// + private static async Task ReloadWithUpdatedTypes(HotReloadClientOperation? hrOp, Window window, Type[] updatedTypes) { var handlerActions = ElementAgent?.ElementHandlerActions; var uiUpdating = true; try { - var window = CurrentWindow; - if (window is null || !await ShouldReload()) + hrOp?.SetCurrent(); + + if (await ShouldReload() is { value: false } prevent) { uiUpdating = false; + hrOp?.ReportIgnored(prevent.reason); return; } @@ -217,6 +219,8 @@ private static async Task ReloadWithUpdatedTypes(Type[] updatedTypes) } catch (Exception ex) { + hrOp?.ReportError(ex); + if (_log.IsEnabled(LogLevel.Error)) { _log.Error($"Error doing UI Update - {ex.Message}", ex); @@ -228,6 +232,9 @@ private static async Task ReloadWithUpdatedTypes(Type[] updatedTypes) { // Action: ReloadCompleted _ = handlerActions?.Do(h => h.Value.ReloadCompleted(updatedTypes, uiUpdating)).ToArray(); + + hrOp?.ResignCurrent(); + hrOp?.ReportCompleted(); } } @@ -420,12 +427,12 @@ public static void ForceHotReloadUpdate() { try { - _source = HotReloadSource.Manual; + _instance?._status.ConfigureSourceForNextOperation(HotReloadSource.Manual); UpdateApplication(Array.Empty()); } finally { - _source = default; + _instance?._status.ConfigureSourceForNextOperation(default); } } @@ -435,7 +442,7 @@ public static void ForceHotReloadUpdate() [EditorBrowsable(EditorBrowsableState.Never)] public static void UpdateApplication(Type[] types) { - // TODO: Diag.Report --> Real handler or force reload + var hr = _instance?._status.ReportLocalStarting(types); foreach (var type in types) { @@ -459,6 +466,7 @@ public static void UpdateApplication(Type[] types) { _log.Error($"Error while processing MetadataUpdateOriginalTypeAttribute for {type}", error); } + hr?.ReportError(error); } } @@ -468,21 +476,24 @@ public static void UpdateApplication(Type[] types) } #if WINUI - var dispatcherQueue = CurrentWindow?.DispatcherQueue; - if (dispatcherQueue is not null) + if (CurrentWindow is { DispatcherQueue: { } dispatcherQueue } window) { - dispatcherQueue.TryEnqueue(async () => await ReloadWithUpdatedTypes(types)); + dispatcherQueue.TryEnqueue(async () => await ReloadWithUpdatedTypes(hr, window, types)); } #else - var dispatcher = CurrentWindow?.Dispatcher; - if (dispatcher is not null) + if (CurrentWindow is { Dispatcher: { } dispatcher } window) { - _ = dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, async () => await ReloadWithUpdatedTypes(types)); + _ = dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, async () => await ReloadWithUpdatedTypes(hr, window, types)); } #endif - else if (_log.IsEnabled(LogLevel.Warning)) + else { - _log.Warn($"Unable to access Dispatcher/DispatcherQueue in order to invoke {nameof(ReloadWithUpdatedTypes)}. Make sure you have enabled hot-reload (Window.EnableHotReload()) in app startup. See https://aka.platform.uno/hot-reload"); + var errorMsg = $"Unable to access Dispatcher/DispatcherQueue in order to invoke {nameof(ReloadWithUpdatedTypes)}. Make sure you have enabled hot-reload (Window.EnableHotReload()) in app startup. See https://aka.platform.uno/hot-reload"; + hr?.ReportError(new InvalidOperationException(errorMsg)); + if (_log.IsEnabled(LogLevel.Warning)) + { + _log.Warn(errorMsg); + } } } } diff --git a/src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.cs b/src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.cs index 3c0fe0a86163..c12a29b50e8a 100644 --- a/src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.cs +++ b/src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.cs @@ -8,6 +8,10 @@ using Uno.Foundation.Logging; using Uno.UI.RemoteControl.HotReload.Messages; using Uno.Diagnostics.UI; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Reflection; +using Uno.UI.Helpers; namespace Uno.UI.RemoteControl.HotReload; @@ -16,7 +20,6 @@ public partial class ClientHotReloadProcessor : IClientProcessor private string? _projectPath; private string[]? _xamlPaths; private readonly IRemoteControlClient _rcClient; - private readonly DiagnosticView _diagView = DiagnosticView.Register("Hot reload", (view, status) => view.Update(status)); private HotReloadMode? _forcedHotReloadMode; private Dictionary? _msbuildProperties; @@ -38,7 +41,7 @@ public async Task ProcessFrame(Messages.Frame frame) switch (frame.Name) { case AssemblyDeltaReload.Name: - AssemblyReload(frame.GetContent()); + ProcessAssemblyReload(frame.GetContent()); break; case FileReload.Name: @@ -50,7 +53,7 @@ public async Task ProcessFrame(Messages.Frame frame) break; case HotReloadStatusMessage.Name: - await ProcessStatus(frame.GetContent()); + await ProcessServerStatus(frame.GetContent()); break; default: @@ -157,8 +160,8 @@ private string GetMSBuildProperty(string property, string defaultValue = "") } #endregion - private async Task ProcessStatus(HotReloadStatusMessage status) + private async Task ProcessServerStatus(HotReloadStatusMessage status) { - _diagView.Update(status); + _status.ReportServerStatus(status); } } diff --git a/src/Uno.UI.RemoteControl/HotReload/HotReloadStatusView.cs b/src/Uno.UI.RemoteControl/HotReload/HotReloadStatusView.cs index e5affa957a1b..843ec546dd77 100644 --- a/src/Uno.UI.RemoteControl/HotReload/HotReloadStatusView.cs +++ b/src/Uno.UI.RemoteControl/HotReload/HotReloadStatusView.cs @@ -1,54 +1,160 @@ using System; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Globalization; +using System.IO; using System.Linq; +using Windows.UI; +using Microsoft.UI; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; using Uno.UI.RemoteControl.HotReload.Messages; +using static Uno.UI.RemoteControl.HotReload.ClientHotReloadProcessor; namespace Uno.UI.RemoteControl.HotReload; internal sealed partial class HotReloadStatusView : Control { - private (long id, string state) _currentResult = (-1, "None"); + #region HeadLine (DP) + public static DependencyProperty HeadLineProperty { get; } = DependencyProperty.Register( + nameof(HeadLine), + typeof(string), + typeof(HotReloadStatusView), + new PropertyMetadata(default(string), (snd, args) => ToolTipService.SetToolTip(snd, args.NewValue?.ToString()))); + + public string HeadLine + { + get => (string)GetValue(HeadLineProperty); + set => SetValue(HeadLineProperty, value); + } + #endregion + + #region History (DP) + public static DependencyProperty HistoryProperty { get; } = DependencyProperty.Register( + nameof(History), + typeof(ObservableCollection), + typeof(HotReloadStatusView), + new PropertyMetadata(default(ObservableCollection))); + + public ObservableCollection History + { + get => (ObservableCollection)GetValue(HistoryProperty); + private init => SetValue(HistoryProperty, value); + } + #endregion public HotReloadStatusView() { DefaultStyleKey = typeof(HotReloadStatusView); + History = []; + + UpdateHeadline(null); } - /// - protected override void OnApplyTemplate() + public void Update(Status status) { - base.OnApplyTemplate(); + SyncOperations(status); + UpdateHeadline(status.State); + + VisualStateManager.GoToState(this, GetStatusVisualState(status.State), true); + VisualStateManager.GoToState(this, GetResultVisualState(), true); } - public void Update(HotReloadStatusMessage? status) + private void SyncOperations(Status status) { - ToolTipService.SetToolTip(this, GetStatusSummary(status)); + var operations = History; + var vms = operations.ToDictionary(op => (op.IsServer, op.Id)); - if (status is null) + foreach (var srvOp in status.Server.Operations) { - return; + if (!vms.TryGetValue((true, srvOp.Id), out var vm)) + { + vm = new HotReloadEntryViewModel(true, srvOp.Id, srvOp.StartTime); + operations.Insert(FindIndex(srvOp.StartTime), vm); + } + + string[] files = srvOp.FilePaths.Select(Path.GetFileName).ToArray()!; + + vm.IsCompleted = srvOp.Result is not null; + vm.IsSuccess = srvOp.Result is HotReloadServerResult.Success or HotReloadServerResult.NoChanges; + vm.Description = srvOp.Result switch + { + null => $"Processing changes{Join(files, "files")}.", + HotReloadServerResult.NoChanges => $"No changes detected by the server{Join(files, "files")}.", + HotReloadServerResult.Success => $"Server successfully detected and compiled changes{Join(files, "files")}.", + HotReloadServerResult.RudeEdit => $"Server detected changes{Join(files, "files")} but is not able to apply them.", + HotReloadServerResult.Failed => $"Server detected changes{Join(files, "files")} but is not able to compile them.", + HotReloadServerResult.Aborted => $"Hot-reload has been aborted (usually because some other changes has been detected).", + HotReloadServerResult.InternalError => "Hot-reload failed for due to an internal error.", + _ => $"Unknown server operation result: {srvOp.Result}." + }; + vm.Duration = srvOp.EndTime is not null ? srvOp.EndTime - srvOp.StartTime : null; + vm.RaiseChanged(); } - VisualStateManager.GoToState(this, GetStatusVisualState(status), true); - if (GetResultVisualState(status) is { } resultState) + foreach (var localOp in status.Local.Operations) { - VisualStateManager.GoToState(this, resultState, true); + if (!vms.TryGetValue((false, localOp.Id), out var vm)) + { + vm = new HotReloadEntryViewModel(false, localOp.Id, localOp.StartTime); + operations.Insert(FindIndex(localOp.StartTime), vm); + } + + var types = localOp.CuratedTypes; + + vm.IsCompleted = localOp.Result is not null; + vm.IsSuccess = localOp.Result is HotReloadClientResult.Success; + vm.Description = localOp.Result switch + { + null => $"Processing changes{Join(types, "types")} (total of {localOp.Types.Length} types updated).", + HotReloadClientResult.Success => $"Application received {localOp.Types.Length} changes{Join(types, "types")} and updated the view (total of {localOp.Types.Length} types updated).", + HotReloadClientResult.Failed => $"Application received {localOp.Types.Length} changes{Join(types, "types")} (total of {localOp.Types.Length} types updated) but failed to update the view ({localOp.Exceptions.FirstOrDefault()?.Message}).", + HotReloadClientResult.Ignored => $"Application received {localOp.Types.Length} changes{Join(types, "types")} (total of {localOp.Types.Length} types updated) but view was not been updated because {localOp.IgnoreReason}.", + _ => $"Unknown application operation result: {localOp.Result}." + }; + vm.Duration = localOp.EndTime is not null ? localOp.EndTime - localOp.StartTime : null; + vm.RaiseChanged(); + } + + string Join(string[] items, string itemType, int maxItems = 5) + => items switch + { + { Length: 0 } => "", + { Length: 1 } => $" in {items[0]}", + { Length: < 3 } => $" in {string.Join(",", items[..^1])} and {items[^1]}", + _ => $" in {string.Join(",", items[..3])} and {items.Length - 3} other {itemType}" + }; + + int FindIndex(DateTimeOffset date) + { + for (var i = 0; i < operations.Count; i++) + { + if (operations[i].Start > date) + { + return i - 1; + } + } + + return 0; } } - public static string GetStatusSummary(HotReloadStatusMessage? status) - => status?.State switch + public string UpdateHeadline(HotReloadState? state) + => HeadLine = state switch { - HotReloadState.Disabled => "Hot-reload is disabled.", - HotReloadState.Initializing => "Hot-reload is initializing.", + null => """ + State of the hot-reload engine is unknown. + This usually indicates that connection to the dev-server failed, but if running within VisualStudio, updates might still be detected. + """, + HotReloadState.Disabled => "Hot-reload server is disabled.", + HotReloadState.Initializing => "Hot-reload engine is initializing.", HotReloadState.Idle => "Hot-reload server is ready and listening for file changes.", - HotReloadState.Processing => "Hot-reload server is processing file changes", - _ => "Unable to determine the state of the hot-reload server." + HotReloadState.Processing => "Hot-reload engine is processing file changes", + _ => "Unable to determine the state of the hot-reload engine." }; - private static string GetStatusVisualState(HotReloadStatusMessage status) - => status.State switch + private static string GetStatusVisualState(HotReloadState state) + => state switch { HotReloadState.Disabled => "Disabled", HotReloadState.Initializing => "Initializing", @@ -57,30 +163,47 @@ private static string GetStatusVisualState(HotReloadStatusMessage status) _ => "Unknown" }; - private string? GetResultVisualState(HotReloadStatusMessage status) + private string GetResultVisualState() { - var op = status.Operations.MaxBy(op => op.Id); - if (op is null) + var operations = History; + if (operations is { Count: 0 } || operations.Any(op => !op.IsCompleted)) { - return null; // No state change + return "None"; // Makes sure to restore to previous None! } - var updated = (op.Id, GetStateName(op)); - if (_currentResult == updated) - { - return null; // No state change - } + return operations[0].IsSuccess ? "Success" : "Failed"; + } +} - _currentResult = updated; - return _currentResult.state; +[Microsoft.UI.Xaml.Data.Bindable] +internal sealed record HotReloadEntryViewModel(bool IsServer, long Id, DateTimeOffset Start) : INotifyPropertyChanged +{ + /// + public event PropertyChangedEventHandler? PropertyChanged; - static string GetStateName(HotReloadOperationInfo op) - => op.Result switch - { - null => "None", - HotReloadResult.NoChanges => "Success", - HotReloadResult.Success => "Success", - _ => "Failed" - }; - } + public bool IsCompleted { get; set; } + public bool IsSuccess { get; set; } + public TimeSpan? Duration { get; set; } + public string? Description { get; set; } + + // Quick patch as we don't have MVUX + public string Title => $"{Start.LocalDateTime:T} - {(IsServer ? "Server" : "Application")}{GetDuration()}".ToString(CultureInfo.CurrentCulture); + public Color Color => (IsCompleted, IsSuccess) switch + { + (false, _) => Colors.Yellow, + (true, false) => Colors.Red, + (true, true) => Colors.Green, + }; + + public void RaiseChanged() + => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("")); + + private string GetDuration() + => Duration switch + { + null => string.Empty, + { TotalMilliseconds: < 1000 } ms => $" - {ms.TotalMilliseconds:F0} ms", + { TotalSeconds: < 3 } s => $" - {s.TotalSeconds:N2} s", + { } d => $" - {d:g}" + }; } diff --git a/src/Uno.UI.RemoteControl/HotReload/HotReloadStatusView.xaml b/src/Uno.UI.RemoteControl/HotReload/HotReloadStatusView.xaml index be7b2db60429..c35000738736 100644 --- a/src/Uno.UI.RemoteControl/HotReload/HotReloadStatusView.xaml +++ b/src/Uno.UI.RemoteControl/HotReload/HotReloadStatusView.xaml @@ -4,14 +4,28 @@ xmlns:local="using:Uno.UI.RemoteControl.HotReload" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:controls="clr-namespace:Microsoft.UI.Xaml.Controls;assembly=Uno.UI" mc:Ignorable="d" d:DesignHeight="300" d:DesignWidth="400"> + + + + + + + + + + + + + diff --git a/src/Uno.UI.RemoteControl/HotReload/Messages/HotReloadResult.cs b/src/Uno.UI.RemoteControl/HotReload/Messages/HotReloadServerResult.cs similarity index 89% rename from src/Uno.UI.RemoteControl/HotReload/Messages/HotReloadResult.cs rename to src/Uno.UI.RemoteControl/HotReload/Messages/HotReloadServerResult.cs index 1ac7d3736454..8a5f19d1905b 100644 --- a/src/Uno.UI.RemoteControl/HotReload/Messages/HotReloadResult.cs +++ b/src/Uno.UI.RemoteControl/HotReload/Messages/HotReloadServerResult.cs @@ -4,9 +4,9 @@ namespace Uno.UI.RemoteControl.HotReload.Messages; /// -/// The result of an hot-reload operation. +/// The result of a hot-reload operation on server. /// -public enum HotReloadResult +public enum HotReloadServerResult { /// /// Hot-reload completed with no changes. diff --git a/src/Uno.UI.RemoteControl/HotReload/Messages/HotReloadStatusMessage.cs b/src/Uno.UI.RemoteControl/HotReload/Messages/HotReloadStatusMessage.cs index ed3810fd5c31..673a414d1ca0 100644 --- a/src/Uno.UI.RemoteControl/HotReload/Messages/HotReloadStatusMessage.cs +++ b/src/Uno.UI.RemoteControl/HotReload/Messages/HotReloadStatusMessage.cs @@ -7,7 +7,7 @@ namespace Uno.UI.RemoteControl.HotReload.Messages; public record HotReloadStatusMessage( [property: JsonProperty] HotReloadState State, - [property: JsonProperty] IImmutableList Operations) + [property: JsonProperty] IImmutableList Operations) : IMessage { public const string Name = nameof(HotReloadStatusMessage); @@ -21,4 +21,9 @@ public record HotReloadStatusMessage( string IMessage.Name => Name; } -public record HotReloadOperationInfo(long Id, ImmutableHashSet FilePaths, HotReloadResult? Result); +public record HotReloadServerOperationData( + long Id, + DateTimeOffset StartTime, + ImmutableHashSet FilePaths, + DateTimeOffset? EndTime, + HotReloadServerResult? Result); diff --git a/src/Uno.UI.RemoteControl/HotReload/MetadataUpdater/ElementUpdaterAgent.cs b/src/Uno.UI.RemoteControl/HotReload/MetadataUpdater/ElementUpdaterAgent.cs index cc60e4463eb1..d9dff7580042 100644 --- a/src/Uno.UI.RemoteControl/HotReload/MetadataUpdater/ElementUpdaterAgent.cs +++ b/src/Uno.UI.RemoteControl/HotReload/MetadataUpdater/ElementUpdaterAgent.cs @@ -23,14 +23,16 @@ internal sealed class ElementUpdateAgent : IDisposable private const DynamicallyAccessedMemberTypes HotReloadHandlerLinkerFlags = DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods; private readonly Action _log; + private readonly Action _onActionError; private readonly AssemblyLoadEventHandler _assemblyLoad; private readonly ConcurrentDictionary _elementHandlerActions = new(); internal const string MetadataUpdaterType = "System.Reflection.Metadata.MetadataUpdater"; - public ElementUpdateAgent(Action log) + public ElementUpdateAgent(Action log, Action onActionError) { _log = log; + _onActionError = onActionError; _assemblyLoad = OnAssemblyLoad; AppDomain.CurrentDomain.AssemblyLoad += _assemblyLoad; LoadElementUpdateHandlerActions(); @@ -314,6 +316,7 @@ internal void GetElementHandlerActions( } catch (Exception ex) { + _onActionError(update, ex); _log($"Exception from '{update.Name}' on {handlerType.Name}: {ex}"); } }; diff --git a/src/Uno.UI.RemoteControl/RemoteControlClient.Diagnostics.cs b/src/Uno.UI.RemoteControl/RemoteControlClient.Diagnostics.cs index 6493a6ff4bcc..62d77e869e98 100644 --- a/src/Uno.UI.RemoteControl/RemoteControlClient.Diagnostics.cs +++ b/src/Uno.UI.RemoteControl/RemoteControlClient.Diagnostics.cs @@ -98,7 +98,7 @@ private class DiagnosticsSink : IDiagnosticsSink { private ConnectionState _state = ConnectionState.Idle; private readonly DiagnosticView _view = DiagnosticView.Register( - "Dev-server", + "Dev-server", (view, status) => view.Update(status), RemoteControlStatusView.GetStatusDetails); diff --git a/src/Uno.UI/Diagnostics/DiagnosticView.Factories.cs b/src/Uno.UI/Diagnostics/DiagnosticView.Factories.cs index d214e8e68a04..0db66f889fa5 100644 --- a/src/Uno.UI/Diagnostics/DiagnosticView.Factories.cs +++ b/src/Uno.UI/Diagnostics/DiagnosticView.Factories.cs @@ -23,9 +23,9 @@ public static DiagnosticView Register( Func? details = null) where TView : FrameworkElement, new() { - var provider = details is null + var provider = details is null ? new DiagnosticView(name, () => new TView(), update) - : new DiagnosticView(name, () => new TView(), update, (ctx, state, ct) => new(details(state)); + : new DiagnosticView(name, () => new TView(), update, (ctx, state, ct) => new(details(state))); DiagnosticViewRegistry.Register(provider); return provider; } diff --git a/src/Uno.UI/Helpers/TypeMappings.cs b/src/Uno.UI/Helpers/TypeMappings.cs index f71df7da3780..9937fe533e51 100644 --- a/src/Uno.UI/Helpers/TypeMappings.cs +++ b/src/Uno.UI/Helpers/TypeMappings.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Text; +using System.Threading; using System.Threading.Tasks; namespace Uno.UI.Helpers; @@ -165,12 +166,10 @@ public static void Resume() /// Indicates whether the layout should be updated after resuming updates public static void Resume(bool updateLayout) { - var completion = _mappingsPaused; - _mappingsPaused = null; - if (completion is not null) + if (Interlocked.Exchange(ref _mappingsPaused, null) is { } completion) { - MappedTypeToOriginalTypeMappings = AllMappedTypeToOriginalTypeMappings.ToDictionary(x => x.Key, x => x.Value); - OriginalTypeToMappedType = AllOriginalTypeToMappedType.ToDictionary(x => x.Key, x => x.Value); + MappedTypeToOriginalTypeMappings = new Dictionary(AllMappedTypeToOriginalTypeMappings); + OriginalTypeToMappedType = new Dictionary(AllOriginalTypeToMappedType); completion.TrySetResult(updateLayout); } }