using XFESystem.Desktop; namespace XFESystem.Browser; /// /// The UI owns this mailbox, never the underlying Jint engine. Its paint and /// data getters read complete immutable snapshots. All script, DOM, layout and /// input work continues on the one verified worker after the first ready frame. /// public sealed class BackgroundBrowserEditorSession : IBackgroundEditorSession { private const int Capacity = 512; private sealed record Command(Action? Apply, int ReadbackId = 0, bool PointerMove = false); private sealed record Completion(int Id, string? Source, string? Error); private sealed record Size(int Width, int Height); private sealed record Snapshot(long Sequence, BrowserPage Page, string Source, BrowserPaintSnapshot Paint, double Height, bool TextCursor, BackgroundEditorDiagnostics Diagnostics); private readonly BrowserService page; private readonly IEditorBackgroundWorkScheduler scheduler; private readonly Func clock; private readonly Func threadIdentity; private readonly Action? trace; private readonly EditorPreparationRequest request; private readonly Command?[] commands = new Command?[Capacity]; private readonly Completion?[] completions = new Completion?[Capacity]; private readonly Dictionary> readbacks = []; private uint commandPublished, commandConsumed, completionPublished, completionConsumed; private int scheduled, cancelled, nextReadback; private long requestedTime, lastQueuedTime, observedSequence, nextSequence; private long capturedPageRevision; private Size size; private Snapshot snapshot; private string? uiError; private static long nextPaintIdentity; private readonly long paintIdentity = Interlocked.Increment(ref nextPaintIdentity); public long RasterPaintCount { get; private set; } public long CachedPaintCount { get; private set; } internal BackgroundBrowserEditorSession(BrowserService page, IEditorBackgroundWorkScheduler scheduler, EditorPreparationRequest request, Func clock, Func threadIdentity, Action? trace, Func? preparationCancellation = null) { this.page = page; this.scheduler = scheduler; this.request = request; this.clock = clock; this.threadIdentity = threadIdentity; this.trace = trace; size = new(request.ViewportWidth, request.ViewportHeight); // Installed on the worker: publication must not disconnect the // resource/script boundary checks from a later UI-side close. page.SetOperationCancellation(() => Volatile.Read(ref cancelled) != 0 || preparationCancellation?.Invoke() == true); requestedTime = lastQueuedTime = clock(); snapshot = Capture(); } public BrowserPage CurrentPage => Volatile.Read(ref snapshot).Page; public long Revision => Volatile.Read(ref snapshot).Sequence; public double DocumentHeight => Volatile.Read(ref snapshot).Height; public bool TextCursor => Volatile.Read(ref snapshot).TextCursor; public BackgroundEditorDiagnostics Diagnostics { get { var value = Volatile.Read(ref snapshot).Diagnostics; return uiError is null ? value : value with { Error = uiError }; } } public string? ReadEditorText() => Volatile.Read(ref snapshot).Source; public void SetCanvas(IDesktopCanvas canvas) { } public void CompleteEditorPreparationHandoff(IDesktopCanvas canvas, bool equivalentTextMetrics) { } public BrowserPage Navigate(string address) => throw new NotSupportedException("This background session owns one code document."); public void ResizeViewport(int width, int height) { var current = Volatile.Read(ref size); if (current.Width == width && current.Height == height) return; Volatile.Write(ref size, new(Math.Max(1, width), Math.Max(1, height))); EnsureQueued(); } public bool Pointer(BrowserPointerInput input) => Enqueue(new(browser => { browser.Pointer(input); }, PointerMove: input.Type == "mousemove")); public bool Key(BrowserKeyboardInput input) => Enqueue(new(browser => { browser.Key(input); })); public bool RequestEditorReadback(Action completed) { ArgumentNullException.ThrowIfNull(completed); if (readbacks.Count >= 128 || Volatile.Read(ref cancelled) != 0) return false; int id = ++nextReadback; readbacks.Add(id, completed); if (Enqueue(new(null, id))) return true; readbacks.Remove(id); return false; } public bool AdvanceTime(long monotonicMilliseconds) { DrainCompletions(); long sequence = Volatile.Read(ref snapshot).Sequence; bool changed = sequence != observedSequence; observedSequence = sequence; Volatile.Write(ref requestedTime, Math.Max(Volatile.Read(ref requestedTime), monotonicMilliseconds)); if (Volatile.Read(ref commandPublished) != Volatile.Read(ref commandConsumed) || monotonicMilliseconds - lastQueuedTime >= 50) EnsureQueued(); return changed; } public void Cancel() { Volatile.Write(ref cancelled, 1); readbacks.Clear(); } public void Paint(IDesktopCanvas canvas, DesktopBounds bounds, double scrollY) { var frame = Volatile.Read(ref snapshot); var cache = canvas as IDesktopWebFrameCache; if (cache is not null && cache.TryRestoreWebFrame(paintIdentity, frame.Sequence, frame.Diagnostics.LayoutPassCount, bounds, 0, 0)) { CachedPaintCount++; return; } frame.Paint.Paint(canvas, bounds); RasterPaintCount++; cache?.StoreWebFrame(paintIdentity, frame.Sequence, frame.Diagnostics.LayoutPassCount, bounds, 0, 0); } private bool Enqueue(Command command) { if (Volatile.Read(ref cancelled) != 0) return false; uint writer = Volatile.Read(ref commandPublished), reader = Volatile.Read(ref commandConsumed); // Atomically replace only an unconsumed tail move. Exchanging the slot // with null on the consumer makes a failed replacement enqueue a new // event instead, so the final position cannot disappear in a race. if (command.PointerMove && writer != reader) { int tail = (int)(unchecked(writer - 1) & (Capacity - 1)); var previous = Volatile.Read(ref commands[tail]); if (previous is { PointerMove: true } && ReferenceEquals(Interlocked.CompareExchange(ref commands[tail], command, previous), previous)) { EnsureQueued(); return true; } } if (unchecked(writer - reader) >= Capacity) { uiError = "Editor input queue is full; input was rejected."; return false; } commands[writer & (Capacity - 1)] = command; Volatile.Write(ref commandPublished, unchecked(writer + 1)); EnsureQueued(); return true; } private void EnsureQueued() { if (Volatile.Read(ref cancelled) != 0 || Interlocked.CompareExchange(ref scheduled, 1, 0) != 0) return; lastQueuedTime = Volatile.Read(ref requestedTime); if (scheduler.TryQueue(Work)) { uiError = null; return; } Volatile.Write(ref scheduled, 0); uiError = "Editor worker queue is busy; queued input will retry."; } private void Work() { var finishedReadbacks = new List(); try { WorkCore(finishedReadbacks); } catch (Exception error) when (error is not OutOfMemoryException and not StackOverflowException) { if (Volatile.Read(ref cancelled) != 0) { Volatile.Write(ref scheduled, 0); return; } var previous = Volatile.Read(ref snapshot); Volatile.Write(ref snapshot, previous with { Sequence = ++nextSequence, Diagnostics = previous.Diagnostics with { Error = BoundedText.Safe(error.Message, 512) } }); trace?.Invoke("XFE_EDITOR_BACKGROUND|SessionError|Window|" + request.WindowId + "|Generation|" + request.Generation + "|Thread|" + threadIdentity() + "\n"); foreach (var completion in finishedReadbacks) PublishCompletion(completion with { Error = BoundedText.Safe(error.Message, 512) }); } // The pinned native dispatcher does not support arbitrary finally // unwinding. WorkCore may return early on cancellation; its caller // always clears this publication flag on the explicit common path. Volatile.Write(ref scheduled, 0); } private void WorkCore(List finishedReadbacks) { if (Volatile.Read(ref cancelled) != 0) return; var target = Volatile.Read(ref size); page.ResizeViewport(target.Width, target.Height); int processed = 0; while (processed < Capacity) { uint reader = Volatile.Read(ref commandConsumed); if (reader == Volatile.Read(ref commandPublished)) break; processed++; var command = Interlocked.Exchange(ref commands[reader & (Capacity - 1)], null)!; Volatile.Write(ref commandConsumed, unchecked(reader + 1)); if (Volatile.Read(ref cancelled) != 0) return; if (command.ReadbackId != 0) { try { finishedReadbacks.Add(new(command.ReadbackId, page.ReadEditorText(), null)); } catch (Exception error) when (error is not OutOfMemoryException and not StackOverflowException) { finishedReadbacks.Add(new(command.ReadbackId, null, BoundedText.Safe(error.Message, 512))); } } else command.Apply!(page); } page.AdvanceTime(Math.Max(clock(), Volatile.Read(ref requestedTime))); if (Volatile.Read(ref cancelled) != 0) return; var previous = Volatile.Read(ref snapshot); bool changed = processed != 0 || page.Revision != capturedPageRevision || previous.Paint.Viewport.Width != target.Width || previous.Paint.Viewport.Height != target.Height; var completed = changed ? Capture() : previous; if (Volatile.Read(ref cancelled) == 0) { Volatile.Write(ref snapshot, completed); // A save acknowledgement cannot overtake the model snapshot // containing the input that this FIFO readback already saw. foreach (var completion in finishedReadbacks) PublishCompletion(completion); } } private Snapshot Capture() { var target = Volatile.Read(ref size); // Resize may have arrived during a long callback. Dispatch the real // resize before layout, rather than letting Layout alone consume the // new size and suppress Monaco's next resize notification. page.ResizeViewport(target.Width, target.Height); page.Layout(new(target.Width, target.Height)); var paint = page.CaptureEditorPaintSnapshot(); string source = page.ReadEditorText() ?? throw new IOException("The actual editor model is unavailable."); var diagnostics = new BackgroundEditorDiagnostics(page.ReadMonacoProof(), page.LastInputTrace, page.LayoutPassCount, page.ScrollLayoutReuseCount, page.PackagedScriptCacheHits, page.PackagedScriptPreparations, threadIdentity()); capturedPageRevision = page.Revision; return new(++nextSequence, page.CurrentPage, source, paint, page.DocumentHeight, page.TextCursor, diagnostics); } private void PublishCompletion(Completion completion) { uint writer = Volatile.Read(ref completionPublished), reader = Volatile.Read(ref completionConsumed); if (unchecked(writer - reader) >= Capacity) throw new IOException("Editor completion queue is full."); completions[writer & (Capacity - 1)] = completion; Volatile.Write(ref completionPublished, unchecked(writer + 1)); } private void DrainCompletions() { while (true) { uint reader = Volatile.Read(ref completionConsumed); if (reader == Volatile.Read(ref completionPublished)) return; var completion = completions[reader & (Capacity - 1)]!; completions[reader & (Capacity - 1)] = null; Volatile.Write(ref completionConsumed, unchecked(reader + 1)); if (readbacks.Remove(completion.Id, out var callback)) callback(completion.Source, completion.Error); } } }