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>
/// 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.
/// </summary>
public sealed class BackgroundBrowserEditorSession : IBackgroundEditorSession
{
    private const int Capacity = 512;
    private sealed record Command(Action<BrowserService>? 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<long> clock;
    private readonly Func<int> threadIdentity;
    private readonly Action<string>? trace;
    private readonly EditorPreparationRequest request;
    private readonly Command?[] commands = new Command?[Capacity];
    private readonly Completion?[] completions = new Completion?[Capacity];
    private readonly Dictionary<int, Action<string?, string?>> 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<long> clock, Func<int> threadIdentity, Action<string>? trace, Func<bool>? 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<string?, string?> 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<Completion>();
        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<Completion> 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);
        }
    }
}