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,
    IEditorBackgroundWorkScheduler? navigationScheduler = 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(request).TryQueue(() => Prepare(operation)))
            { operation.Fail("The verified background editor worker is unavailable.", 0); operation.FinishWorker(); }
        }
        catch (Exception error) when (error is not OutOfMemoryException and not StackOverflowException)
        { operation.Fail(BoundedText.Safe(error.Message, 512), 0); operation.FinishWorker(); }
        return operation;
    }

    private void Prepare(Operation operation)
    {
        BrowserService? ownedPage=null;EditorPreparationResult? completed=null;bool transferred=false;
        try
        {
            completed=PrepareCore(operation,browser=>ownedPage=browser);
            if(completed is not null)transferred=operation.Complete(completed);
        }
        catch(Exception error)when(error is not OutOfMemoryException and not StackOverflowException)
        {
            if(operation.Cancelled)operation.Report("Cancelled",ThreadId);
            else operation.Fail(BoundedText.Safe(error.Message,512),ThreadId);
        }
        // Common explicit owner cleanup also covers every cancelled early
        // return, failed initialization, and cancelled publication CAS.
        if(!transferred)
        {
            if(completed?.Page is BackgroundBrowserEditorSession background)background.RetireOnOwner();
            else ownedPage?.Release();
        }
        operation.FinishWorker();
    }

    private EditorPreparationResult? PrepareCore(Operation operation,Action<BrowserService> ownPage)
    {
        int workerThread = ThreadId;
        if (operation.Cancelled) { operation.Report("Cancelled", workerThread); return null; }
        operation.Report("WorkBegin", workerThread);
            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.");
            ownPage(browser);
            browser.SetOperationCancellation(() => operation.Cancelled);
            page.SetCanvas(canvas);
            page.ResizeViewport(request.ViewportWidth, request.ViewportHeight);
            switch (request.Kind)
            {
                case EditorPreparationKind.Editor: page.NavigateEditor(request.Source, request.Path); break;
                case EditorPreparationKind.Document: page.BeginDocumentPreview(request.Source, request.Path, request.Dark); break;
                case EditorPreparationKind.Navigation: page.Navigate(request.Path); break;
                default: throw new ArgumentOutOfRangeException(nameof(request.Kind));
            }
            BrowserDisplayList? layout = null;
            int initializationPumps = 0;
            for (; initializationPumps <= 8; initializationPumps++)
            {
                if (operation.Cancelled) { operation.Report("Cancelled", workerThread); return null; }
                if (request.Kind != EditorPreparationKind.Editor) break;
                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 null; }
            operation.Report("FrameReady", workerThread, initializationPumps);
            if (operation.Cancelled) { operation.Report("Cancelled", workerThread); return null; }
            // 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.
            // Native snapshots already contain immutable glyph coverage. No
            // font dictionaries need copying or re-rasterizing on the UI.
            var deliveryCanvas = request.Kind == EditorPreparationKind.Editor && canvas is not IDesktopWebTextPreparationCanvas
                ? canvasFactory.CreateEditorPreparationCanvas() : null;
            if (deliveryCanvas is IEditorPreparationCanvasHandoff cache)
                foreach (var command in layout!.Commands)
                {
                    if (operation.Cancelled) { operation.Report("Cancelled", workerThread); return null; }
                    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 (request.Kind == EditorPreparationKind.Editor && 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 null; }
            var session = new BackgroundBrowserEditorSession(browser, Scheduler(request), request,
                () => Clock(request, 136), () => ThreadId, trace, () => operation.Cancelled);
            operation.Report("Prepared", workerThread);
            return 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.
    }

    private long Clock(EditorPreparationRequest request, int fallbackAdvance) =>
        Math.Max(request.MonotonicMilliseconds, monotonicMilliseconds?.Invoke() ?? checked(request.MonotonicMilliseconds + fallbackAdvance));
    private IEditorBackgroundWorkScheduler Scheduler(EditorPreparationRequest request) =>
        request.Kind == EditorPreparationKind.Navigation ? navigationScheduler ?? scheduler : scheduler;

    private sealed class Operation(EditorPreparationRequest request, int uiThread, Action<string>? trace) : IEditorPreparationOperation
    {
        private sealed record Publication(EditorPreparationResult? Result,bool Cancelled=false,bool Finished=false);
        private Publication publication=new(null);
        public EditorPreparationRequest Request => request;
        public bool Cancelled => Volatile.Read(ref publication).Cancelled;
        public bool IsRetired
        {get{var state=Volatile.Read(ref publication);return state.Finished&&(state.Result?.Page is not IBackgroundEditorSession background||background.IsRetired);}}
        public void Cancel()
        {
            while(true)
            {
                var state=Volatile.Read(ref publication);if(state.Cancelled)break;
                if(ReferenceEquals(Interlocked.CompareExchange(ref publication,state with{Cancelled=true},state),state))break;
            }
            if(Volatile.Read(ref publication).Result?.Page is IBackgroundEditorSession background)background.Release();
        }
        public void AdvanceRetirement(long monotonicMilliseconds)
        {
            var state=Volatile.Read(ref publication);if(!state.Cancelled)return;
            if(state.Result?.Page is IBackgroundEditorSession background)
            {
                background.Release();background.AdvanceTime(monotonicMilliseconds);
                if(!background.IsRetired)return;
                Interlocked.CompareExchange(ref publication,state with{Result=null},state);
            }
        }
        public bool TryTakeResult(out EditorPreparationResult completed)
        {
            while(true)
            {
                var state=Volatile.Read(ref publication);
                if(state.Cancelled||state.Result is null){completed=null!;return false;}
                if(ReferenceEquals(Interlocked.CompareExchange(ref publication,state with{Result=null},state),state))
                {completed=state.Result;return true;}
            }
        }
        public bool Complete(EditorPreparationResult completed)
        {
            while(true)
            {
                var state=Volatile.Read(ref publication);if(state.Cancelled)return false;
                if(ReferenceEquals(Interlocked.CompareExchange(ref publication,state with{Result=completed},state),state))return true;
            }
        }
        public void FinishWorker()
        {while(true){var state=Volatile.Read(ref publication);if(state.Finished||ReferenceEquals(Interlocked.CompareExchange(ref publication,state with{Finished=true},state),state))return;}}
        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 + "|Kind|" + request.Kind + "|SourceCharacters|" + request.Source.Length +
            (initializationPumps < 0 ? "" : "|InitializationPumps|" + initializationPumps) + "\n");
    }
}