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, bool ModelInput = 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, long DocumentGeneration, bool NeedsPresentation, 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, retired, nextReadback; private long lastLifetimeReport; private long requestedTime, lastQueuedTime, observedSequence, nextSequence; private long capturedPageRevision; private long nextDocumentProof; private string lastDocumentProof = "{}"; private Size size; private Snapshot snapshot; private bool observedTextCursor; private string? uiError; private readonly long paintIdentity = BrowserPaintIdentity.Next(); private long acknowledgedGeneration, paintedGeneration; private bool paintedNeedsPresentation; private DesktopBounds paintedBounds; private int paintedScale; private IDesktopPresentedPixelsCanvas? paintedCanvas; private long paintedPresentationBefore, paintedSequence; private double paintedScrollY; private string paintedAddress = ""; private bool paintAwaitingPresentation; public long RasterPaintCount { get; private set; } public long CachedPaintCount { get; private set; } public bool IsRetired=>Volatile.Read(ref retired)!=0; 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 double ScrollOffsetY => Volatile.Read(ref snapshot).Paint.Viewport.ScrollY; 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) { ArgumentNullException.ThrowIfNull(address); Enqueue(new(browser => browser.Navigate(address))); return CurrentPage; } 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 => { bool prevented = browser.Pointer(input); if (input.Type == "wheel" && !prevented) browser.ScrollBackgroundDocument(input.DeltaY); }, PointerMove: input.Type == "mousemove")); public bool Key(BrowserKeyboardInput input) => Enqueue(new(browser => { browser.Key(input); }, ModelInput: true)); 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) { if(Volatile.Read(ref cancelled)!=0){EnsureQueued();return false;} DrainCompletions(); var current = Volatile.Read(ref snapshot); long sequence = current.Sequence; bool changed = sequence != observedSequence || current.TextCursor != observedTextCursor; observedSequence = sequence; observedTextCursor = current.TextCursor; 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(); EnsureQueued(); } public void Release()=>Cancel(); 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++; RecordPaint(); return; } frame.Paint.Paint(canvas, bounds); RasterPaintCount++; cache?.StoreWebFrame(paintIdentity, frame.Sequence, frame.Diagnostics.LayoutPassCount, bounds, 0, 0); RecordPaint(); void RecordPaint() { paintedGeneration = frame.DocumentGeneration; paintedNeedsPresentation = frame.NeedsPresentation; paintedBounds = bounds; paintedScale = canvas.ScalePercent; paintedCanvas = canvas as IDesktopPresentedPixelsCanvas; paintedPresentationBefore = paintedCanvas?.FramesPresented ?? 0; paintedSequence = frame.Sequence; paintedScrollY = frame.Paint.Viewport.ScrollY; paintedAddress = frame.Page.Address; paintAwaitingPresentation = true; } } public void FramePresented() { if (!paintAwaitingPresentation || paintedCanvas is { } scanout && scanout.FramesPresented <= paintedPresentationBefore) return; paintAwaitingPresentation = false; long sequence = paintedCanvas?.FramesPresented ?? 0, ticks = paintedCanvas?.LastPresentTicks ?? 0; if (sequence > 0) trace?.Invoke("XFE_BROWSER_BACKGROUND_PRESENTED|Window|" + request.WindowId + "|Generation|" + request.Generation + "|Address|" + paintedAddress + "|SnapshotSequence|" + paintedSequence + "|PresentationSequence|" + sequence + "|PresentationTicks|" + ticks + "|ScrollY|" + paintedScrollY.ToString("0.###", System.Globalization.CultureInfo.InvariantCulture) + "\n"); if (!paintedNeedsPresentation || paintedGeneration == acknowledgedGeneration) return; var bounds = paintedBounds; int scale = paintedScale; long generation = paintedGeneration; if (Enqueue(new(browser => browser.BackgroundFramePresented(bounds, scale, generation, sequence, ticks)))) acknowledgedGeneration = generation; } 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 (IsRetired || 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(); bool succeeded = false; try { WorkCore(finishedReadbacks); succeeded = true; } catch (Exception error) when (error is not OutOfMemoryException and not StackOverflowException) { if(Volatile.Read(ref cancelled)==0) { 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) }); } } // Keep publication cleanup on a common path, including cancellation // and the explicit Legacy runtime entry, which has limited unwinding. Volatile.Write(ref scheduled, 0); if(Volatile.Read(ref cancelled)!=0)RetireOnOwner(); // Continue finite loading work without waiting for the UI's 50 ms // timer between every library. Each script remains a separate FIFO // work item, so input and other pages can run between them. Never // spin while waiting for the first actual presentation or after error. else if (succeeded && page.DocumentEnhancementsPending && !page.NeedsDocumentFirstPresentation) EnsureQueued(); } internal void RetireOnOwner() { if(IsRetired)return; page.Release();Array.Clear(commands);Array.Clear(completions); Volatile.Write(ref retired,1); trace?.Invoke("XFE_EDITOR_BACKGROUND|Retired|Window|"+request.WindowId+"|Generation|"+request.Generation+"|Thread|"+threadIdentity()+"\n"); } 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; bool modelInput = false; 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)!; modelInput |= command.ModelInput || command.ReadbackId != 0; 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; long now=clock(); ReportDocumentProof(now); if(now-lastLifetimeReport>=5000&&page.ReadLifetimeDiagnostics() is { } lifetime) { lastLifetimeReport=now; var weak=page.ReadWeakCollectionLifetime(); trace?.Invoke("XFE_BROWSER_RETENTION|Window|"+request.WindowId+"|Wrappers|"+lifetime.Wrappers+ "|Identities|"+lifetime.Identities+"|Canvases|"+lifetime.Canvases+"|Ranges|"+lifetime.Ranges+ "|Created|"+lifetime.HandlesCreated+"|Disposed|"+lifetime.HandlesDisposed+"|Outstanding|"+lifetime.OutstandingHandles+ "|WeakTables|"+(weak?.Tables??0)+"|WeakEntries|"+(weak?.Entries??0)+ "|WeakCreated|"+(weak?.Created??0)+"|WeakDisposed|"+(weak?.Disposed??0)+"\n"); } var previous = Volatile.Read(ref snapshot); string? source = modelInput ? page.ReadEditorText() : previous.Source; bool changed = page.Revision != capturedPageRevision || source != previous.Source || previous.Paint.Viewport != page.BackgroundViewport || previous.DocumentGeneration != page.DocumentGeneration; // An input acknowledgement is not a new painted frame. No-op // mouse motion and readback retain the immutable paint identity, // native glyph coverage and UI frame cache. Real hover/DOM/canvas // mutations, model changes and scrolling still publish new paint. var completed = previous; if (changed) completed = Capture(); else { bool textCursor = page.TextCursor, needsPresentation = page.NeedsDocumentFirstPresentation; if (textCursor != previous.TextCursor || needsPresentation != previous.NeedsPresentation || page.LastInputTrace != previous.Diagnostics.LastInputTrace) completed = previous with { TextCursor = textCursor, NeedsPresentation = needsPresentation, Diagnostics = page.LastInputTrace == previous.Diagnostics.LastInputTrace ? previous.Diagnostics : previous.Diagnostics with { LastInputTrace = page.LastInputTrace } }; } 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); var paint = page.CaptureEditorPaintSnapshot(); string? source = page.ReadEditorText(); if (request.Kind == EditorPreparationKind.Editor && source is null) 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()); // Bind changed DOM readback to this immutable capture before release // publication. A delayed unchanged poll cannot prove a later scanout. ReportDocumentProof(clock(), force: true); if (trace is not null) { string remoteProof = page.ReadRemotePageProof(nextSequence + 1); if (remoteProof != "{}") trace("XFE_REMOTE_PAGE_PROOF_BEGIN\n" + remoteProof + "\nXFE_REMOTE_PAGE_PROOF_END\n"); } capturedPageRevision = page.Revision; return new(++nextSequence, page.DocumentGeneration, page.NeedsDocumentFirstPresentation, page.CurrentPage, source, paint, page.DocumentHeight, page.TextCursor, diagnostics); } private void ReportDocumentProof(long now, bool force = false) { if (trace is not null && (force || now >= nextDocumentProof)) { nextDocumentProof = now + 500; string proof = page.ReadWebsiteDocumentProof(); if (proof != "{}" && proof != lastDocumentProof) { lastDocumentProof = proof; trace?.Invoke("XFE_WEBSITE_DOCUMENT_PROOF_BEGIN\n" + proof + "\nXFE_WEBSITE_DOCUMENT_PROOF_END\n"); } } } 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); } } }