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 + ",\"SelectorEvaluations\":" + layoutEngine.CssSelectorEvaluations +
",\"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;
}
}
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 + ",\"SelectorEvaluations\":" + layoutEngine.CssSelectorEvaluations +
",\"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;
}
}