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

XFESystem

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

公开
关注 0 Fork 0 Star 0
UTF-8
using AngleSharp.Dom;
using System.Globalization;
using XFESystem.Desktop;

namespace XFESystem.Browser;

public sealed partial class BrowserService
{
    private readonly Queue<IElement> pendingDocumentScripts = [];
    private bool documentEnhancementsPending, awaitingDocumentPresentation, documentPaintAwaitingPresentation;
    private long documentPreviewStarted;
    private string documentSourceFingerprint = "", documentFirstPaintProof = "{}";
    private int documentSourceCharacters;
    private DesktopBounds documentPaintBounds;
    private int documentPaintScale;
    private IDesktopPresentedPixelsCanvas? documentPaintCanvas;
    private long documentPaintSequenceBefore;
    private long documentPresentationSequence, documentPresentationTicks;
    private long documentMarkdownTicks, documentHtmlTicks, documentStyleFetchTicks, documentStylesheetParseTicks,
        documentLayoutTicks, documentRasterTicks, documentFrameCacheTicks;

    /// <summary>True until every original local-document script and load event has run.</summary>
    public bool DocumentEnhancementsPending => documentEnhancementsPending;
    public bool DocumentFirstFramePresented { get; private set; }
    internal long DocumentGeneration { get; private set; }
    internal bool NeedsDocumentFirstPresentation => awaitingDocumentPresentation;
    private int DocumentHostStyleCount => linkedStyles.Count(style =>
        style.Owner.GetAttribute("href") == "/css/docs/xfesystem-desktop.css");

    private void ResetDocumentLoading()
    {
        DocumentGeneration++;
        pendingDocumentScripts.Clear();
        documentEnhancementsPending = awaitingDocumentPresentation = documentPaintAwaitingPresentation = false;
        DocumentFirstFramePresented = false;
        documentFirstPaintProof = "{}";
        documentPaintCanvas = null;
        documentPaintSequenceBefore = -1;
        documentPresentationSequence = documentPresentationTicks = 0;
        documentMarkdownTicks = documentHtmlTicks = documentStyleFetchTicks = documentStylesheetParseTicks =
            documentLayoutTicks = documentRasterTicks = documentFrameCacheTicks = 0;
    }

    public void FramePresented() => WithGate(() =>
    {
        if (!awaitingDocumentPresentation || !documentPaintAwaitingPresentation) return 0;
        if (documentPaintCanvas is not null && documentPaintCanvas.FramesPresented <= documentPaintSequenceBefore) return 0;
        long presentedAt = System.Diagnostics.Stopwatch.GetTimestamp();
        awaitingDocumentPresentation = documentPaintAwaitingPresentation = false;
        DocumentFirstFramePresented = true;
        if (documentPaintCanvas is not null)
        { documentPresentationSequence = documentPaintCanvas.FramesPresented; documentPresentationTicks = documentPaintCanvas.LastPresentTicks; }
        documentFirstPaintProof = BuildDocumentFirstPaintProof();
        Trace?.Invoke("XFE_DOCUMENT_FIRST_PRESENTED|Ticks|" + (presentedAt - documentPreviewStarted) +
            "|Frequency|" + System.Diagnostics.Stopwatch.Frequency + "|OriginalStyles|" + (linkedStyles.Count - DocumentHostStyleCount) +
            "|ExecutedScripts|" + executedScripts.Count + "|HostStyles|" + DocumentHostStyleCount +
            "|PresentationSequence|" + documentPresentationSequence + "|PresentationTicks|" + documentPresentationTicks + "\n");
        Trace?.Invoke("XFE_DOCUMENT_FIRST_PROOF_BEGIN\n" + documentFirstPaintProof + "\nXFE_DOCUMENT_FIRST_PROOF_END\n");
        return 0;
    });

    // The UI sends only value geometry after presenting an immutable snapshot.
    // No framebuffer or mutable canvas is shared with the page worker.
    internal void BackgroundFramePresented(DesktopBounds bounds, int scale, long generation, long presentationSequence = 0, long presentationTicks = 0)
    {
        if (!awaitingDocumentPresentation || generation != DocumentGeneration) return;
        documentPaintBounds = bounds; documentPaintScale = scale;
        documentPresentationSequence = presentationSequence; documentPresentationTicks = presentationTicks;
        documentPaintCanvas = null; documentPaintAwaitingPresentation = true;
        FramePresented();
    }

    // One classic script is an indivisible interpreter evaluation. This
    // returns to the desktop between scripts; it does not claim arbitrary
    // script preemption or execute Jint on the runtime's unsafe worker EH.
    private bool AdvanceDocumentLoading()
    {
        if (!documentEnhancementsPending || awaitingDocumentPresentation) return false;
        if (runtime is null)
        {
            InitializeDocumentRuntime();
            return true;
        }
        if (pendingResources.Count != 0)
        {
            DrainResources(1);
            return true;
        }
        if (pendingDocumentScripts.TryDequeue(out var script))
        {
            if (!RunScript(script)) pendingDocumentScripts.Clear();
            return true;
        }
        documentEnhancementsPending = false;
        dom!.CompleteLoading();
        CaptureEventOutput();
        InvalidateDocument();
        TraceDocumentReady();
        return true;
    }

    private void InitializeDocumentRuntime()
    {
        runtime = new(activeLimits);
        dom = new(runtime, document!, currentAddress!, activeLimits, CreateDomHost());
    }

    private void TraceDocumentReady() => Trace?.Invoke("XFE_DOCUMENT_PREVIEW_READY|Ticks|" +
        (System.Diagnostics.Stopwatch.GetTimestamp() - documentPreviewStarted) +
        "|Frequency|" + System.Diagnostics.Stopwatch.Frequency + "\n");

    /// <summary>Captured from the real DOM/layout at presentation, without executing JavaScript or mutating its layout.</summary>
    public string ReadDocumentFirstPaintProof() => documentFirstPaintProof;

    private static string SourceFingerprint(string source)
    {
        // Explicit UTF-16LE FNV-1a fingerprint, for correlating the source with
        // the host's SHA-256-bound fixture. This is not a cryptographic digest.
        ulong hash = 14695981039346656037;
        foreach (char character in source)
        {
            hash = unchecked((hash ^ (byte)character) * 1099511628211);
            hash = unchecked((hash ^ (byte)(character >> 8)) * 1099511628211);
        }
        return hash.ToString("x16", CultureInfo.InvariantCulture);
    }

    private string BuildDocumentFirstPaintProof()
    {
        if (document is null || displayList is null) return "{}";
        string Number(double value) => value.ToString("0.###", CultureInfo.InvariantCulture);
        string Bounds(BrowserRect value) => "{\"X\":" + Number(value.X) + ",\"Y\":" + Number(value.Y) +
            ",\"Width\":" + Number(value.Width) + ",\"Height\":" + Number(value.Height) + "}";
        string PresentedPixels(IElement element)
        {
            if (documentPaintCanvas is null) return "null";
            BrowserRect union = default;
            foreach (var command in displayList.Commands)
                if (command.Kind == BrowserDrawKind.Text && IsDescendant(command.Node, element) && !string.IsNullOrWhiteSpace(command.Text))
                    union = union.Union(command.Bounds.Translate(-viewport.ScrollX, -viewport.ScrollY));
            union = union.Intersect(new(0, 0, documentPaintBounds.Width, documentPaintBounds.Height));
            if (union.Width <= 0 || union.Height <= 0) return "null";
            double scale = documentPaintScale / 100d;
            int left = (int)Math.Floor((documentPaintBounds.X + union.X) * scale), top = (int)Math.Floor((documentPaintBounds.Y + union.Y) * scale);
            int right = (int)Math.Ceiling((documentPaintBounds.X + union.Right) * scale), bottom = (int)Math.Ceiling((documentPaintBounds.Y + union.Bottom) * scale);
            if (!documentPaintCanvas.TryFingerprintPresentedPixels(new(left, top, right - left, bottom - top), out var fingerprint)) return "null";
            return "{\"FingerprintAlgorithm\":" + JavaScriptString(DesktopPresentedPixels.FingerprintAlgorithm) + ",\"Fingerprint\":" + JavaScriptString(fingerprint.Fingerprint) +
                ",\"NativeBounds\":{\"X\":" + fingerprint.NativeBounds.X + ",\"Y\":" + fingerprint.NativeBounds.Y + ",\"Width\":" + fingerprint.NativeBounds.Width +
                ",\"Height\":" + fingerprint.NativeBounds.Height + "},\"PresentationSequence\":" + fingerprint.PresentationSequence + "}";
        }
        string Element(IElement? element, bool capturePixels = false)
        {
            if (element is null) return "null";
            var style = displayList.GetComputedStyle(element);
            var runs = displayList.Commands.Where(command => command.Kind == BrowserDrawKind.Text && IsDescendant(command.Node, element)).Take(16)
                .Select(command => "{\"Text\":" + JavaScriptString(command.Text) + ",\"Color\":" + command.Color +
                    ",\"Bounds\":" + Bounds(command.Bounds.Translate(-viewport.ScrollX, -viewport.ScrollY)) + "}");
            return "{\"Text\":" + JavaScriptString(BoundedText.Safe(element.TextContent, 2048)) + ",\"Bounds\":" + Bounds(displayList.GetBoundingClientRect(element)) +
                ",\"Color\":" + (style?.Color.Rgb ?? 0) + ",\"BackgroundColor\":" + (style?.Background.Rgb ?? 0) +
                ",\"BackgroundAlpha\":" + Number(style?.Background.Alpha ?? 0) + ",\"FontSize\":" + Number(style?.Font.Size ?? 0) +
                ",\"Runs\":[" + string.Join(',', runs) + "]" + (capturePixels ? ",\"PresentedPixels\":" + PresentedPixels(element) : "") + "}";
        }
        var article = document.QuerySelector(".markdown-body");
        return "{\"Kind\":\"ActualNativeFirstDocumentPaint\",\"ReadOnly\":true,\"JavaScriptExecutedForProof\":false," +
            "\"NoHostScreenshotSubstitution\":true,\"Address\":" + JavaScriptString(currentAddress!.AbsoluteUri) +
            ",\"SourceFingerprintAlgorithm\":\"UTF16LE-FNV1a64\",\"SourceFingerprint\":" + JavaScriptString(documentSourceFingerprint) +
            ",\"SourceCharacters\":" + documentSourceCharacters + ",\"OriginalStyleCount\":" + (linkedStyles.Count - DocumentHostStyleCount) +
            ",\"HostStyleCount\":" + DocumentHostStyleCount +
            ",\"PresentationSequence\":" + documentPresentationSequence +
            ",\"ExecutedScriptCount\":" + executedScripts.Count + ",\"ScalePercent\":" + documentPaintScale +
            ",\"FirstPaintPhases\":{\"ClockFrequency\":" + System.Diagnostics.Stopwatch.Frequency + ",\"MarkdownConvertTicks\":" + documentMarkdownTicks +
            ",\"HtmlParseTicks\":" + documentHtmlTicks + ",\"StylesheetFetchTicks\":" + documentStyleFetchTicks + ",\"StylesheetParseTicks\":" + documentStylesheetParseTicks +
            ",\"LayoutTicks\":" + documentLayoutTicks + ",\"LayoutIncludesStylesheetParsing\":true,\"RasterPaintTicks\":" + documentRasterTicks +
            ",\"FrameCacheStoreTicks\":" + documentFrameCacheTicks + ",\"PresentationTicks\":" + documentPresentationTicks + "}" +
            ",\"Viewport\":{\"X\":" + documentPaintBounds.X + ",\"Y\":" + documentPaintBounds.Y + ",\"Width\":" + documentPaintBounds.Width +
            ",\"Height\":" + documentPaintBounds.Height + "},\"Heading\":" + Element(article?.QuerySelector("h1,h2,h3"), true) +
            ",\"Paragraph\":" + Element(article?.QuerySelector("p"), true) + ",\"Article\":" + Element(article) + "}";
    }

    private static bool IsDescendant(INode? node, INode ancestor)
    {
        while (node is not null) { if (ReferenceEquals(node, ancestor)) return true; node = node.Parent; }
        return false;
    }
}