using XFESystem.Desktop; namespace XFESystem.Browser; /// /// A page and its Jint engine belong exclusively to one worker for their entire /// lifetime. Only immutable paint/model snapshots are published to the desktop. /// The scheduler and isolated real-font canvas are explicit platform capabilities. /// public sealed class BrowserEditorPreparationService( IInteractiveBrowserFactory factory, IEditorBackgroundWorkScheduler scheduler, IEditorPreparationCanvasFactory canvasFactory, Action? trace = null, Func? threadIdentity = null, Func? monotonicMilliseconds = null) : IEditorPreparationService { private int ThreadId => threadIdentity?.Invoke() ?? Environment.CurrentManagedThreadId; public IEditorPreparationOperation Begin(EditorPreparationRequest request) { ArgumentNullException.ThrowIfNull(request); ArgumentNullException.ThrowIfNull(request.Source); ArgumentNullException.ThrowIfNull(request.Path); var operation = new Operation(request, ThreadId, trace); operation.Report("Begin", ThreadId); try { if (!scheduler.TryQueue(() => Prepare(operation))) operation.Fail("The verified background editor worker is unavailable.", 0); } catch (Exception error) when (error is not OutOfMemoryException and not StackOverflowException) { operation.Fail(BoundedText.Safe(error.Message, 512), 0); } return operation; } private void Prepare(Operation operation) { int workerThread = ThreadId; if (operation.Cancelled) { operation.Report("Cancelled", workerThread); return; } operation.Report("WorkBegin", workerThread); try { var request = operation.Request; if (request.ViewportWidth is < 1 or > 100_000 || request.ViewportHeight is < 1 or > 100_000) throw new ArgumentOutOfRangeException(nameof(request), "Invalid editor viewport."); // Both page construction and private font setup can be substantial; // neither is performed by Begin or by the desktop's polling loop. var canvas = canvasFactory.CreateEditorPreparationCanvas(); if (canvas is not IDesktopWebCanvas) throw new NotSupportedException("Editor preparation requires actual text metrics and rasterization."); var page = factory.CreatePageSession(); if (page is not BrowserService browser) throw new NotSupportedException("Background editor pages require the actual managed browser engine."); browser.SetOperationCancellation(() => operation.Cancelled); page.SetCanvas(canvas); page.ResizeViewport(request.ViewportWidth, request.ViewportHeight); page.NavigateEditor(request.Source, request.Path); BrowserDisplayList? layout = null; int initializationPumps = 0; for (; initializationPumps <= 8; initializationPumps++) { if (operation.Cancelled) { operation.Report("Cancelled", workerThread); return; } layout = browser.ReadyEditorLayout(request.Source, request.Path, request.ViewportWidth, request.ViewportHeight); if (layout is not null) break; if (initializationPumps == 8) throw new IOException("Monaco did not produce the actual source view before the initialization limit."); // The desktop and worker use the SAME clock domain. A fixed // 17..136 clock left long-delayed initialization callbacks for // the UI's first 20,000+ ms pump, freezing the handoff there. page.AdvanceTime(Clock(request, (initializationPumps + 1) * 17)); } if (operation.Cancelled) { operation.Report("Cancelled", workerThread); return; } operation.Report("FrameReady", workerThread, initializationPumps); if (operation.Cancelled) { operation.Report("Cancelled", workerThread); return; } // Caret/RAF/observer work is perpetual. Readiness means a real model // and frame exist, not that all browser timers can become silent. // A SECOND private canvas is delivered to the UI. The continuing // worker must never share its mutable fonts/caches with scanout. var deliveryCanvas = canvasFactory.CreateEditorPreparationCanvas(); if (deliveryCanvas is IEditorPreparationCanvasHandoff cache) foreach (var command in layout!.Commands) { if (operation.Cancelled) { operation.Report("Cancelled", workerThread); return; } if (command.Kind == BrowserDrawKind.Text && command.Opacity > 0 && command.Text.Length != 0 && command.Bounds.Intersect(new(0, 0, request.ViewportWidth, request.ViewportHeight)) is { Width: > 0, Height: > 0 }) cache.PrepareEditorText(command.Text, command.Font.Size, command.Font.Family, command.Font.Weight); } if (page.ReadEditorText() != request.Source) throw new IOException("The prepared Monaco model did not preserve the source text."); if (operation.Cancelled) { operation.Report("Cancelled", workerThread); return; } var session = new BackgroundBrowserEditorSession(browser, scheduler, request, () => Clock(request, 136), () => ThreadId, trace, () => operation.Cancelled); if (operation.Cancelled) { session.Cancel(); operation.Report("Cancelled", workerThread); return; } operation.Report("Prepared", workerThread); operation.Complete(new(session, null, workerThread, deliveryCanvas)); // Only the proxy crosses into the UI. The actual page remains // worker-owned for timers, input, model readback and future layout. } catch (Exception error) when (error is not OutOfMemoryException and not StackOverflowException) { if (operation.Cancelled) operation.Report("Cancelled", workerThread); else operation.Fail(BoundedText.Safe(error.Message, 512), workerThread); } } private long Clock(EditorPreparationRequest request, int fallbackAdvance) => Math.Max(request.MonotonicMilliseconds, monotonicMilliseconds?.Invoke() ?? checked(request.MonotonicMilliseconds + fallbackAdvance)); private sealed class Operation(EditorPreparationRequest request, int uiThread, Action? trace) : IEditorPreparationOperation { private EditorPreparationResult? result; private int cancelled; public EditorPreparationRequest Request => request; public bool Cancelled => Volatile.Read(ref cancelled) != 0; public void Cancel() => Volatile.Write(ref cancelled, 1); public bool TryTakeResult(out EditorPreparationResult completed) { completed = Interlocked.Exchange(ref result, null)!; return completed is not null; } public void Complete(EditorPreparationResult completed) { if (!Cancelled) Volatile.Write(ref result, completed); } public void Fail(string message, int workerThread) { Report("Failed", workerThread); Complete(new(null, message, workerThread)); } public void Report(string state, int workerThread, int initializationPumps = -1) => trace?.Invoke("XFE_EDITOR_BACKGROUND|" + state + "|Window|" + request.WindowId + "|Generation|" + request.Generation + "|UiThread|" + uiThread + "|Thread|" + workerThread + "|SourceCharacters|" + request.Source.Length + (initializationPumps < 0 ? "" : "|InitializationPumps|" + initializationPumps) + "\n"); } }