Skip to content

Commit

Permalink
feat: Add disagnostic indocator for hot-reload process
Browse files Browse the repository at this point in the history
  • Loading branch information
dr1rrb committed Jun 17, 2024
1 parent 8d2600b commit 3d92130
Show file tree
Hide file tree
Showing 26 changed files with 1,213 additions and 244 deletions.
287 changes: 206 additions & 81 deletions src/Uno.UI.RemoteControl.Host/RemoteControlServer.cs

Large diffs are not rendered by default.

13 changes: 13 additions & 0 deletions src/Uno.UI.RemoteControl.Messaging/IServerProcessor.cs
Original file line number Diff line number Diff line change
@@ -1,14 +1,27 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Uno.UI.RemoteControl.HotReload.Messages;
using Uno.UI.RemoteControl.Messaging.IdeChannel;

namespace Uno.UI.RemoteControl.Host
{
public interface IServerProcessor : IDisposable
{
string Scope { get; }

/// <summary>
/// Processes a frame from the Client
/// </summary>
/// <param name="frame">The frame received from the client.</param>
Task ProcessFrame(Frame frame);

/// <summary>
/// Processes a message from the IDE
/// </summary>
/// <param name="message">The message received from the IDE.</param>
/// <param name="ct">The cancellation token.</param>
Task ProcessIdeMessage(IdeMessage message, CancellationToken ct);
}

[System.AttributeUsage(AttributeTargets.Assembly, Inherited = false, AllowMultiple = true)]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,25 +1,38 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Uno.Extensions;
using Uno.UI.RemoteControl.HotReload.Messages;
using Uno.UI.RemoteControl.Messaging.IdeChannel;

[assembly: Uno.UI.RemoteControl.Host.ServerProcessorAttribute(typeof(Uno.UI.RemoteControl.Host.HotReload.FileUpdateProcessor))]

namespace Uno.UI.RemoteControl.Host.HotReload;

partial class FileUpdateProcessor : IServerProcessor, IDisposable
{
// *******************************************
// *******************************************
// ***************** WARNING *****************
// *******************************************
// *******************************************
//
// This processor is present only for legacy purposes.
// The Scope of the UpdateFile message has been changed from WellKnownScopes.Testing to WellKnownScopes.HotReload.
// This processor will only handle requests made on the old scope, like old version of the runtime-test engine.
// The new processor that is handling those messages is now the ServerHotReloadProcessor.

private readonly IRemoteControlServer _remoteControlServer;

public FileUpdateProcessor(IRemoteControlServer remoteControlServer)
{
_remoteControlServer = remoteControlServer;
}

public string Scope => HotReloadConstants.TestingScopeName;
public string Scope => WellKnownScopes.Testing;

public void Dispose()
{
Expand All @@ -37,6 +50,10 @@ public Task ProcessFrame(Frame frame)
return Task.CompletedTask;
}

/// <inheritdoc />
public Task ProcessIdeMessage(IdeMessage message, CancellationToken ct)
=> Task.CompletedTask;

private void ProcessUpdateFile(UpdateFile? message)
{
if (message?.IsValid() is not true)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,26 +17,12 @@ internal static class CompilationWorkspaceProvider
{
private static string MSBuildBasePath = "";

public static Task<(Solution, WatchHotReloadService)> CreateWorkspaceAsync(
public static async Task<(Solution, WatchHotReloadService)> CreateWorkspaceAsync(
string projectPath,
IReporter reporter,
string[] metadataUpdateCapabilities,
Dictionary<string, string> properties,
CancellationToken cancellationToken)
{
var taskCompletionSource = new TaskCompletionSource<(Solution, WatchHotReloadService)>(TaskCreationOptions.RunContinuationsAsynchronously);
CreateProject(taskCompletionSource, projectPath, reporter, metadataUpdateCapabilities, properties, cancellationToken);

return taskCompletionSource.Task;
}

static async void CreateProject(
TaskCompletionSource<(Solution, WatchHotReloadService)> taskCompletionSource,
string projectPath,
IReporter reporter,
string[] metadataUpdateCapabilities,
Dictionary<string, string> properties,
CancellationToken cancellationToken)
CancellationToken ct)
{
if (properties.TryGetValue("UnoEnCLogPath", out var EnCLogPath))
{
Expand Down Expand Up @@ -78,31 +64,30 @@ static async void CreateProject(
reporter.Verbose($"MSBuildWorkspace {diag.Diagnostic}");
};

await workspace.OpenProjectAsync(projectPath, cancellationToken: cancellationToken);
await workspace.OpenProjectAsync(projectPath, cancellationToken: ct);
break;
}
catch (InvalidOperationException) when (i > 1)
{
// When we load the work space right after the app was started, it happens that it "app build" is not yet completed, preventing us to open the project.
// We retry a few times to let the build complete.
await Task.Delay(5_000, cancellationToken);
await Task.Delay(5_000, ct);
}
}
var currentSolution = workspace.CurrentSolution;
var hotReloadService = new WatchHotReloadService(workspace.Services, metadataUpdateCapabilities);
await hotReloadService.StartSessionAsync(currentSolution, cancellationToken);
await hotReloadService.StartSessionAsync(currentSolution, ct);

// Read the documents to memory
await Task.WhenAll(
currentSolution.Projects.SelectMany(p => p.Documents.Concat(p.AdditionalDocuments)).Select(d => d.GetTextAsync(cancellationToken)));
await Task.WhenAll(currentSolution.Projects.SelectMany(p => p.Documents.Concat(p.AdditionalDocuments)).Select(d => d.GetTextAsync(ct)));

// Warm up the compilation. This would help make the deltas for first edit appear much more quickly
foreach (var project in currentSolution.Projects)
{
await project.GetCompilationAsync(cancellationToken);
await project.GetCompilationAsync(ct);
}

taskCompletionSource.TrySetResult((currentSolution, hotReloadService));
return (currentSolution, hotReloadService);
}

public static void InitializeRoslyn(string? workDir)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using System.IO;
using System.Linq;
using System.Reactive.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
Expand All @@ -20,12 +21,16 @@
using Uno.Disposables;
using Uno.Extensions;
using Uno.UI.RemoteControl.Host.HotReload.MetadataUpdates;
using Uno.UI.RemoteControl.HotReload;
using Uno.UI.RemoteControl.HotReload.Messages;
using Uno.UI.RemoteControl.Messaging.HotReload;

namespace Uno.UI.RemoteControl.Host.HotReload
{
partial class ServerHotReloadProcessor : IServerProcessor, IDisposable
{
private static readonly StringComparer _pathsComparer = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal;

private FileSystemWatcher[]? _solutionWatchers;
private CompositeDisposable? _solutionWatcherEventsDisposable;

Expand All @@ -36,7 +41,7 @@ partial class ServerHotReloadProcessor : IServerProcessor, IDisposable

private bool _useRoslynHotReload;

private void InitializeMetadataUpdater(ConfigureServer configureServer)
private bool InitializeMetadataUpdater(ConfigureServer configureServer)
{
_ = bool.TryParse(_remoteControlServer.GetServerConfiguration("metadata-updates"), out _useRoslynHotReload);

Expand All @@ -47,6 +52,12 @@ private void InitializeMetadataUpdater(ConfigureServer configureServer)
CompilationWorkspaceProvider.InitializeRoslyn(Path.GetDirectoryName(configureServer.ProjectPath));

InitializeInner(configureServer);

return true;
}
else
{
return false;
}
}

Expand All @@ -55,6 +66,8 @@ private void InitializeInner(ConfigureServer configureServer) => _initializeTask
{
try
{
await Notify(HotReloadEvent.Initializing);

var result = await CompilationWorkspaceProvider.CreateWorkspaceAsync(
configureServer.ProjectPath,
_reporter,
Expand All @@ -64,15 +77,17 @@ private void InitializeInner(ConfigureServer configureServer) => _initializeTask

ObserveSolutionPaths(result.Item1);

await _remoteControlServer.SendFrame(new HotReloadWorkspaceLoadResult() { WorkspaceInitialized = true });
await _remoteControlServer.SendFrame(new HotReloadWorkspaceLoadResult { WorkspaceInitialized = true });
await Notify(HotReloadEvent.Ready);

return result;
}
catch (Exception e)
{
Console.WriteLine($"Failed to initialize compilation workspace: {e}");

await _remoteControlServer.SendFrame(new HotReloadWorkspaceLoadResult() { WorkspaceInitialized = false });
await _remoteControlServer.SendFrame(new HotReloadWorkspaceLoadResult { WorkspaceInitialized = false });
await Notify(HotReloadEvent.Disabled);

throw;
}
Expand Down Expand Up @@ -115,53 +130,41 @@ private void ObserveSolutionPaths(Solution solution)

foreach (var watcher in _solutionWatchers)
{
// Create an observable instead of using the FromEventPattern which
// does not register to events properly.
// Renames are required for the WriteTemporary->DeleteOriginal->RenameToOriginal that
// Visual Studio uses to save files.

var changes = Observable.Create<string>(o =>
{

void changed(object s, FileSystemEventArgs args) => o.OnNext(args.FullPath);
void renamed(object s, RenamedEventArgs args) => o.OnNext(args.FullPath);

watcher.Changed += changed;
watcher.Created += changed;
watcher.Renamed += renamed;

return Disposable.Create(() =>
{
watcher.Changed -= changed;
watcher.Created -= changed;
watcher.Renamed -= renamed;
});
});

var disposable = changes
.Buffer(TimeSpan.FromMilliseconds(250))
.Subscribe(filePaths =>
{
ProcessMetadataChanges(filePaths.Distinct());
}, e => Console.WriteLine($"Error {e}"));
var disposable = ToObservable(watcher).Subscribe(
filePaths => _ = ProcessMetadataChanges(filePaths.Distinct()),
e => Console.WriteLine($"Error {e}"));

_solutionWatcherEventsDisposable.Add(disposable);

}
}

private void ProcessMetadataChanges(IEnumerable<string> filePaths)
private async Task ProcessMetadataChanges(IEnumerable<string> filePaths)
{
if (_useRoslynHotReload)
if (_useRoslynHotReload) // Note: Always true here?!
{
foreach (var file in filePaths)
var files = filePaths.ToImmutableHashSet(_pathsComparer);
var hotReload = await StartOrContinueHotReload(files);

try
{
ProcessSolutionChanged(CancellationToken.None, file).Wait();
// Note: We should process all files at once here!
foreach (var file in files)
{
ProcessSolutionChanged(hotReload, file, CancellationToken.None).Wait();
}
}
catch (Exception e)
{
_reporter.Warn($"Internal error while processing hot-reload ({e.Message}).");
}
finally
{
await hotReload.CompleteUsingIntermediates();
}
}
}

private async Task<bool> ProcessSolutionChanged(CancellationToken cancellationToken, string file)
private async Task<bool> ProcessSolutionChanged(HotReloadOperation hotReload, string file, CancellationToken cancellationToken)
{
if (!await EnsureSolutionInitializedAsync() || _currentSolution is null || _hotReloadService is null)
{
Expand Down Expand Up @@ -216,10 +219,12 @@ private async Task<bool> ProcessSolutionChanged(CancellationToken cancellationTo
if (diagnostics.IsDefaultOrEmpty)
{
await UpdateMetadata(file, updates);
hotReload.NotifyIntermediate(file, HotReloadResult.NoChanges);
}
else
{
_reporter.Output($"Got {diagnostics.Length} errors");
hotReload.NotifyIntermediate(file, HotReloadResult.Failed);
}

// HotReloadEventSource.Log.HotReloadEnd(HotReloadEventSource.StartType.CompilationHandler);
Expand All @@ -236,6 +241,8 @@ private async Task<bool> ProcessSolutionChanged(CancellationToken cancellationTo
_reporter.Verbose(CSharpDiagnosticFormatter.Instance.Format(diagnostic, CultureInfo.InvariantCulture));
}

hotReload.NotifyIntermediate(file, HotReloadResult.RudeEdit);

// HotReloadEventSource.Log.HotReloadEnd(HotReloadEventSource.StartType.CompilationHandler);
return false;
}
Expand All @@ -245,6 +252,7 @@ private async Task<bool> ProcessSolutionChanged(CancellationToken cancellationTo
sw.Stop();

await UpdateMetadata(file, updates);
hotReload.NotifyIntermediate(file, HotReloadResult.Success);

// HotReloadEventSource.Log.HotReloadEnd(HotReloadEventSource.StartType.CompilationHandler);
return true;
Expand Down Expand Up @@ -347,10 +355,10 @@ private ImmutableArray<string> GetErrorDiagnostics(Solution solution, Cancellati
}


[MemberNotNullWhen(true, nameof(_currentSolution))]
[MemberNotNullWhen(true, nameof(_currentSolution), nameof(_hotReloadService))]
private async ValueTask<bool> EnsureSolutionInitializedAsync()
{
if (_currentSolution != null)
if (_currentSolution is not null && _hotReloadService is not null)
{
return true;
}
Expand Down
Loading

0 comments on commit 3d92130

Please sign in to comment.