using AngleSharp.Dom; using System.Globalization; using XFESystem.Desktop; namespace XFESystem.Browser; public sealed partial class BrowserService { private readonly Queue 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 documentMarkdownTicks, documentHtmlTicks, documentStyleFetchTicks, documentStylesheetParseTicks, documentLayoutTicks, documentRasterTicks, documentFrameCacheTicks; /// True until every original local-document script and load event has run. public bool DocumentEnhancementsPending => documentEnhancementsPending; public bool DocumentFirstFramePresented { get; private set; } private void ResetDocumentLoading() { pendingDocumentScripts.Clear(); documentEnhancementsPending = awaitingDocumentPresentation = documentPaintAwaitingPresentation = false; DocumentFirstFramePresented = false; documentFirstPaintProof = "{}"; documentPaintCanvas = null; documentPaintSequenceBefore = -1; 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; documentFirstPaintProof = BuildDocumentFirstPaintProof(); Trace?.Invoke("XFE_DOCUMENT_FIRST_PRESENTED|Ticks|" + (presentedAt - documentPreviewStarted) + "|Frequency|" + System.Diagnostics.Stopwatch.Frequency + "|OriginalStyles|" + linkedStyles.Count + "|ExecutedScripts|" + executedScripts.Count + "\n"); Trace?.Invoke("XFE_DOCUMENT_FIRST_PROOF_BEGIN\n" + documentFirstPaintProof + "\nXFE_DOCUMENT_FIRST_PROOF_END\n"); return 0; }); // 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"); /// Captured from the real DOM/layout at presentation, without executing JavaScript or mutating its layout. 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 + ",\"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\":" + (documentPaintCanvas?.LastPresentTicks ?? 0) + "}" + ",\"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; } }