XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFESystem

【Rust】XFE系统,基于Rust和汇编

公开
关注 0 Fork 0 Star 0
UTF-8
using XFESystem.Desktop;

namespace XFESystem.Browser;

/// <summary>
/// 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.
/// </summary>
public sealed class BrowserEditorPreparationService(
    IInteractiveBrowserFactory factory, IEditorBackgroundWorkScheduler scheduler,
    IEditorPreparationCanvasFactory canvasFactory, Action<string>? trace = null,
    Func<int>? threadIdentity = null, Func<long>? 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<string>? 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");
    }
}