using System.Diagnostics; using System.Reflection; using System.Security.Cryptography; using System.Text.Json; using XFESystem.Browser; using XFESystem.Desktop; using XFESystem.Documents; // Host stage timing only. This does not measure the native framebuffer/GPU and // never opens a host browser, changes website resources or disables JavaScript. var root = Path.GetFullPath(args.Length > 0 ? args[0] : "."); var label = args.Length > 1 ? args[1] : "Current"; var output = Path.Combine(root, "build/PreviewPerformance", label + "-" + DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()); Directory.CreateDirectory(output); var inputs = new[] { "Source/XFESystem.Browser", "Source/XFESystem.Documents" } .SelectMany(directory => Directory.EnumerateFiles(Path.Combine(root, directory), "*.cs", SearchOption.AllDirectories)) .Where(path => !path.Contains(Path.DirectorySeparatorChar + "obj" + Path.DirectorySeparatorChar) && !path.Contains(Path.DirectorySeparatorChar + "bin" + Path.DirectorySeparatorChar)) .Order(StringComparer.Ordinal).ToDictionary(path => path, Hash); var fixtures = new[] { ("PlainMarkdown.md", "# Preview\n\nHello **XFEstudio**.\n\nSimple document without code or math."), ("CSharp.cs", "using System;\nnamespace XFEstudio;\npublic sealed class Preview { public static void Main() => Console.WriteLine(\"XFE\"); }"), ("WebsiteParity.md", WebsiteAssets.ReadText("Fixtures/WebsiteParity.md")), ("Math.md", "# Math\n\nInline $a+b=c$.\n\n$$\\frac{a}{b}+\\sqrt{x}$$"), ("Diagram.md", "# Diagram\n\n```mermaid\nflowchart LR\nA[Markdown] --> B[Renderer]\n```") }; var rows = new List(); foreach (var (name, source) in fixtures) { var builder = new DocumentPreviewBuilder(new()); for (int attempt = 1; attempt <= 3; attempt++) { var clock = Stopwatch.StartNew(); var html = name.EndsWith(".md", StringComparison.OrdinalIgnoreCase) ? builder.MarkdownWeb(source, name) : builder.CodeWeb(source, name); double htmlMs = clock.Elapsed.TotalMilliseconds; var trace = new List(); var scriptStarts = new Dictionary(); var scriptDurations = new List(); var nav = Stopwatch.StartNew(); var browser = new BrowserService { Trace = line => { double at = nav.Elapsed.TotalMilliseconds; var split = line.Trim().Split('|'); if (split[0] == "XFE_BROWSER_SCRIPT_BEGIN") scriptStarts[split[1]] = at; double? duration = split[0] == "XFE_BROWSER_SCRIPT_END" && scriptStarts.TryGetValue(split[1], out var start) ? at - start : null; if (duration.HasValue) scriptDurations.Add(duration.Value); trace.Add(new { AtMilliseconds = at, DurationMilliseconds = duration, Line = line.Trim() }); } }; using var canvas = new WindowsTextCanvas(Path.Combine(root, "assets/fonts/cascadia-mono-2407.24/CascadiaMono-Regular.ttf"), 796, 404); browser.SetCanvas(canvas); browser.ResizeViewport(796, 404); clock.Restart(); var page = browser.NavigateDocument(source, name); double navigateMs = clock.Elapsed.TotalMilliseconds; var frames = new List(); for (int frame = 1; frame <= 12; frame++) { clock.Restart(); bool changed = browser.AdvanceTime(frame * 20); frames.Add(new { Frame = frame, Changed = changed, Milliseconds = clock.Elapsed.TotalMilliseconds }); } clock.Restart(); var layout = browser.Layout(new(796, 404)); double layoutMs = clock.Elapsed.TotalMilliseconds; clock.Restart(); browser.Paint(canvas, new(0, 0, 796, 404), 0); double paintMs = clock.Elapsed.TotalMilliseconds; var scroll = new List(); for (int index = 1; index <= 8; index++) { clock.Restart(); browser.Paint(canvas, new(0, 0, 796, 404), index * 50); scroll.Add(new { ScrollY = index * 50, Milliseconds = clock.Elapsed.TotalMilliseconds }); } var engine = (BrowserLayoutEngine)typeof(BrowserService).GetField("layoutEngine", BindingFlags.NonPublic | BindingFlags.Instance)!.GetValue(browser)!; var runtime = typeof(BrowserService).GetField("runtime", BindingFlags.NonPublic | BindingFlags.Instance)!.GetValue(browser); long RuntimeCount(string property) => runtime is null ? 0 : (long)typeof(JavaScriptRuntime).GetProperty(property, BindingFlags.NonPublic | BindingFlags.Instance)!.GetValue(runtime)!; bool successful = !browser.CurrentPage.Text.Contains("JavaScript error:", StringComparison.Ordinal) && !browser.CurrentPage.Text.Contains("Event loop failed:", StringComparison.Ordinal) && browser.LoadedResources.Count(resource => resource.Kind == "script") >= 13; rows.Add(new { Name = name, Attempt = attempt, Successful = successful, HtmlGenerationMilliseconds = htmlMs, HtmlCharacters = html.Html.Length, NavigationMilliseconds = navigateMs, ScriptEvaluationMilliseconds = scriptDurations.Sum(), Frames = frames, LayoutMilliseconds = layoutMs, PaintMilliseconds = paintMs, Scroll = scroll, engine.CssParseGeneration, engine.CssReuseCount, LayoutBoxes = layout.Boxes.Count, DisplayCommands = layout.Commands.Count, ViewportPositionedBoxes = layout.Boxes.Where(box => box.Style.Position is "fixed" or "sticky") .Select(box => new { Position = box.Style.Position, box.Visible, Tag = box.Element?.LocalName, Id = box.Element?.Id, Classes = box.Element?.ClassName, box.BorderBox }).ToArray(), PreparedCacheHits = RuntimeCount("PackagedScriptCacheHits"), PreparedScripts = RuntimeCount("PackagedScriptPreparations"), ResourceCharacters = browser.LoadedResources.Sum(resource => resource.Characters), Resources = browser.LoadedResources, Trace = trace, PageText = browser.CurrentPage.Text, browser.Revision, canvas.MeasureCalls, canvas.RasterCalls }); Console.WriteLine(JsonSerializer.Serialize(new { Name = name, Attempt = attempt, Successful = successful, NavigationMilliseconds = navigateMs, ScriptMilliseconds = scriptDurations.Sum(), LayoutMilliseconds = layoutMs, PaintMilliseconds = paintMs })); } } var sourceUnchanged = inputs.All(pair => pair.Value == Hash(pair.Key)); File.WriteAllText(Path.Combine(output, "Result.json"), JsonSerializer.Serialize(new { Kind = "ProductionPreviewPipelineHostProfile", ProductionBrowserServiceUsed = true, HostOnly = true, NativeAcceptance = false, HostRaster = "Offscreen Windows GDI+; not the native rasterizer and not GPU acceleration", SourceUnchangedDuringRun = sourceUnchanged, BrowserAssemblySha256 = Hash(typeof(BrowserService).Assembly.Location), Sources = inputs.Select(pair => new { Path = Path.GetRelativePath(root, pair.Key).Replace('\\', '/'), Sha256 = pair.Value }), Rows = rows }, new JsonSerializerOptions { WriteIndented = true })); Console.WriteLine(Path.Combine(output, "Result.json")); if (!sourceUnchanged) Environment.ExitCode = 1; static string Hash(string path) => Convert.ToHexStringLower(SHA256.HashData(File.ReadAllBytes(path)));