Skip to content

Commit

Permalink
feat(hr): Improve perf and diagnostics per processing updates as batch
Browse files Browse the repository at this point in the history
  • Loading branch information
dr1rrb committed Jun 27, 2024
1 parent b3a3e32 commit 0a0d880
Show file tree
Hide file tree
Showing 5 changed files with 185 additions and 148 deletions.
Original file line number Diff line number Diff line change
@@ -1,27 +1,20 @@
#nullable enable

using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reactive.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Text;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
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;

Expand All @@ -32,7 +25,7 @@ partial class ServerHotReloadProcessor : IServerProcessor, IDisposable
private static readonly StringComparer _pathsComparer = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal;

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

private Task<(Solution, WatchHotReloadService)>? _initializeTask;
private Solution? _currentSolution;
Expand Down Expand Up @@ -103,7 +96,7 @@ private void ObserveSolutionPaths(Solution solution)
.Select(d => d.FilePath)
.Concat(p.AdditionalDocuments
.Select(d => d.FilePath)))
.Select(p => Path.GetDirectoryName(p))
.Select(Path.GetDirectoryName)
.Distinct()
.ToArray();

Expand All @@ -126,155 +119,147 @@ private void ObserveSolutionPaths(Solution solution)
})
.ToArray();

_solutionWatcherEventsDisposable = new CompositeDisposable();
_solutionWatcherEventsDisposable = To2StepsObservable(_solutionWatchers, HasInterest).Subscribe(
filePaths => _ = ProcessMetadataChanges(filePaths),
e => Console.WriteLine($"Error {e}"));

foreach (var watcher in _solutionWatchers)
{
var disposable = ToObservable(watcher).Subscribe(
filePaths => _ = ProcessMetadataChanges(filePaths.Distinct()),
e => Console.WriteLine($"Error {e}"));

_solutionWatcherEventsDisposable.Add(disposable);
}
static bool HasInterest(string file)
=> Path.GetExtension(file).ToLowerInvariant() is not ".csproj" and not ".editorconfig";
}

private async Task ProcessMetadataChanges(IEnumerable<string> filePaths)
private async Task ProcessMetadataChanges(Task<ImmutableHashSet<string>> filesAsync)
{
if (_useRoslynHotReload) // Note: Always true here?!
// Notify the start of the hot-reload processing as soon as possible, even before the buffering of file change is completed
var hotReload = await StartOrContinueHotReload();
var files = await filesAsync;
if (!hotReload.TryMerge(files))
{
var files = filePaths.ToImmutableHashSet(_pathsComparer);
var hotReload = await StartOrContinueHotReload(files);

try
{
// Note: We should process all files at once here!
foreach (var file in files)
{
ProcessSolutionChanged(hotReload, file, CancellationToken.None).Wait();
}
hotReload = await StartHotReload(files);
}

await hotReload.CompleteUsingIntermediates();
}
catch (Exception e)
{
_reporter.Warn($"Internal error while processing hot-reload ({e.Message}).");
try
{
await ProcessSolutionChanged(hotReload, files, CancellationToken.None);
}
catch (Exception e)
{
_reporter.Warn($"Internal error while processing hot-reload ({e.Message}).");

await hotReload.CompleteUsingIntermediates(e);
}
await hotReload.Complete(HotReloadServerResult.InternalError, e);
}
}

private async Task<bool> ProcessSolutionChanged(HotReloadServerOperation hotReload, string file, CancellationToken cancellationToken)
private async ValueTask ProcessSolutionChanged(HotReloadServerOperation hotReload, ImmutableHashSet<string> files, CancellationToken ct)
{
if (!await EnsureSolutionInitializedAsync() || _currentSolution is null || _hotReloadService is null)
{
hotReload.NotifyIntermediate(file, HotReloadServerResult.NoChanges);
return false;
await hotReload.Complete(HotReloadServerResult.Failed); // Failed to init the workspace
return;
}

var sw = Stopwatch.StartNew();

Solution? updatedSolution = null;
ProjectId updatedProjectId;

if (_currentSolution.Projects.SelectMany(p => p.Documents).FirstOrDefault(d => string.Equals(d.FilePath, file, StringComparison.OrdinalIgnoreCase)) is Document documentToUpdate)
{
var sourceText = await GetSourceTextAsync(file);
updatedSolution = documentToUpdate.WithText(sourceText).Project.Solution;
updatedProjectId = documentToUpdate.Project.Id;
}
else if (_currentSolution.Projects.SelectMany(p => p.AdditionalDocuments).FirstOrDefault(d => string.Equals(d.FilePath, file, StringComparison.OrdinalIgnoreCase)) is AdditionalDocument additionalDocument)
// Edit the files in the workspace.
var solution = _currentSolution;
foreach (var file in files)
{
var sourceText = await GetSourceTextAsync(file);
updatedSolution = _currentSolution.WithAdditionalDocumentText(additionalDocument.Id, sourceText, PreservationMode.PreserveValue);
updatedProjectId = additionalDocument.Project.Id;

// Generate an empty document to force the generators to run
// in a separate project of the same solution. This is not needed
// for the head project, but it's no causing issues either.
var docName = Guid.NewGuid().ToString();
updatedSolution = updatedSolution.AddAdditionalDocument(
DocumentId.CreateNewId(updatedProjectId),
docName,
SourceText.From("")
);
if (solution.Projects.SelectMany(p => p.Documents).FirstOrDefault(d => string.Equals(d.FilePath, file, StringComparison.OrdinalIgnoreCase)) is Document documentToUpdate)
{
var sourceText = await GetSourceTextAsync(file);
solution = documentToUpdate.WithText(sourceText).Project.Solution;
}
else if (solution.Projects.SelectMany(p => p.AdditionalDocuments).FirstOrDefault(d => string.Equals(d.FilePath, file, StringComparison.OrdinalIgnoreCase)) is AdditionalDocument additionalDocument)
{
var sourceText = await GetSourceTextAsync(file);
solution = solution.WithAdditionalDocumentText(additionalDocument.Id, sourceText, PreservationMode.PreserveValue);

// Generate an empty document to force the generators to run
// in a separate project of the same solution. This is not needed
// for the head project, but it's no causing issues either.
var docName = Guid.NewGuid().ToString();
solution = solution.AddAdditionalDocument(
DocumentId.CreateNewId(additionalDocument.Project.Id),
docName,
SourceText.From("")
);
}
else
{
_reporter.Verbose($"Could not find document with path {file} in the workspace.");
}
}
else

// Not mater if the build will succeed or not, we update the _currentSolution.
// Files needs to be updated again to fix the compilation errors.
if (solution == _currentSolution)
{
_reporter.Verbose($"Could not find document with path {file} in the workspace.");
// HotReloadEventSource.Log.HotReloadEnd(HotReloadEventSource.StartType.CompilationHandler);
hotReload.NotifyIntermediate(file, HotReloadServerResult.NoChanges);
return false;
_reporter.Output($"No changes found in {string.Join(",", files.Select(Path.GetFileName))}");

await hotReload.Complete(HotReloadServerResult.NoChanges);
return;
}

_currentSolution = solution;

var (updates, hotReloadDiagnostics) = await _hotReloadService.EmitSolutionUpdateAsync(updatedSolution, cancellationToken);
var hasErrorDiagnostics = hotReloadDiagnostics.Any(d => d.Severity == DiagnosticSeverity.Error);
// Compile the solution and get deltas
var (updates, hotReloadDiagnostics) = await _hotReloadService.EmitSolutionUpdateAsync(solution, ct);
// hotReloadDiagnostics currently includes semantic Warnings and Errors for types being updated. We want to limit rude edits to the class
// of unrecoverable errors that a user cannot fix and requires an app rebuild.
var rudeEdits = hotReloadDiagnostics.RemoveAll(d => d.Severity == DiagnosticSeverity.Warning || !d.Descriptor.Id.StartsWith("ENC", StringComparison.Ordinal));

_reporter.Output($"Found {updates.Length} metadata updates after {sw.Elapsed}");
sw.Stop();


if (hasErrorDiagnostics && updates.IsDefaultOrEmpty)
if (rudeEdits.IsEmpty && updates.IsEmpty)
{
// It's possible that there are compilation errors which prevented the solution update
// from being updated. Let's look to see if there are compilation errors.
var diagnostics = GetErrorDiagnostics(updatedSolution, cancellationToken);
if (diagnostics.IsDefaultOrEmpty)
var compilationErrors = GetCompilationErrors(solution, ct);
if (compilationErrors.IsEmpty)
{
await UpdateMetadata(file, updates);
hotReload.NotifyIntermediate(file, HotReloadServerResult.NoChanges);
_reporter.Output("No hot reload changes to apply.");
await hotReload.Complete(HotReloadServerResult.NoChanges);
}
else
{
_reporter.Output($"Got {diagnostics.Length} errors");
hotReload.NotifyIntermediate(file, HotReloadServerResult.Failed);
await hotReload.Complete(HotReloadServerResult.Failed);
}

// HotReloadEventSource.Log.HotReloadEnd(HotReloadEventSource.StartType.CompilationHandler);
// Even if there were diagnostics, continue treating this as a success
return true;
return;
}

if (hasErrorDiagnostics)
if (!rudeEdits.IsEmpty)
{
// Rude edit.
_reporter.Output("Unable to apply hot reload because of a rude edit. Rebuilding the app...");
_reporter.Output("Unable to apply hot reload because of a rude edit.");
foreach (var diagnostic in hotReloadDiagnostics)
{
_reporter.Verbose(CSharpDiagnosticFormatter.Instance.Format(diagnostic, CultureInfo.InvariantCulture));
}

hotReload.NotifyIntermediate(file, HotReloadServerResult.RudeEdit);

// HotReloadEventSource.Log.HotReloadEnd(HotReloadEventSource.StartType.CompilationHandler);
return false;
await hotReload.Complete(HotReloadServerResult.RudeEdit);
return;
}

_currentSolution = updatedSolution;

sw.Stop();
await SendUpdates(updates);

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

// HotReloadEventSource.Log.HotReloadEnd(HotReloadEventSource.StartType.CompilationHandler);
return true;

async Task UpdateMetadata(string file, ImmutableArray<WatchHotReloadService.Update> updates)
async Task SendUpdates(ImmutableArray<WatchHotReloadService.Update> updates)
{
#if DEBUG
_reporter.Output($"Sending {updates.Length} metadata updates for {file}");
_reporter.Output($"Sending {updates.Length} metadata updates for {string.Join(",", files.Select(Path.GetFileName))}");
#endif

for (int i = 0; i < updates.Length; i++)
for (var i = 0; i < updates.Length; i++)
{
var updateTypesWriterStream = new MemoryStream();
var updateTypesWriter = new BinaryWriter(updateTypesWriterStream);
WriteIntArray(updateTypesWriter, updates[i].UpdatedTypes.ToArray());

await _remoteControlServer.SendFrame(
new AssemblyDeltaReload()
new AssemblyDeltaReload
{
FilePath = file,
FilePaths = files,
ModuleId = updates[i].ModuleId.ToString(),
PdbDelta = Convert.ToBase64String(updates[i].PdbDelta.ToArray()),
ILDelta = Convert.ToBase64String(updates[i].ILDelta.ToArray()),
Expand Down Expand Up @@ -319,7 +304,7 @@ private async ValueTask<SourceText> GetSourceTextAsync(string filePath)
return null;
}

private ImmutableArray<string> GetErrorDiagnostics(Solution solution, CancellationToken cancellationToken)
private ImmutableArray<string> GetCompilationErrors(Solution solution, CancellationToken cancellationToken)
{
var @lock = new object();
var builder = ImmutableArray<string>.Empty;
Expand All @@ -331,7 +316,7 @@ private ImmutableArray<string> GetErrorDiagnostics(Solution solution, Cancellati
}

var compilationDiagnostics = compilation.GetDiagnostics(cancellationToken);
if (compilationDiagnostics.IsDefaultOrEmpty)
if (compilationDiagnostics.IsEmpty)
{
return;
}
Expand All @@ -342,7 +327,7 @@ private ImmutableArray<string> GetErrorDiagnostics(Solution solution, Cancellati
if (item.Severity == DiagnosticSeverity.Error)
{
var diagnostic = CSharpDiagnosticFormatter.Instance.Format(item, CultureInfo.InvariantCulture);
_reporter.Output(diagnostic);
_reporter.Output("\x1B[40m\x1B[31m" + diagnostic);
projectDiagnostics = projectDiagnostics.Add(diagnostic);
}
}
Expand All @@ -356,7 +341,6 @@ private ImmutableArray<string> GetErrorDiagnostics(Solution solution, Cancellati
return builder;
}


[MemberNotNullWhen(true, nameof(_currentSolution), nameof(_hotReloadService))]
private async ValueTask<bool> EnsureSolutionInitializedAsync()
{
Expand Down
Loading

0 comments on commit 0a0d880

Please sign in to comment.