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";
if (args.Contains("--loading", StringComparer.Ordinal)) { LoadingProfile.Run(root, label); return; }
if (args.Contains("--monaco", StringComparer.Ordinal)) { MonacoProfile.Run(root, label, args.Contains("--regex", StringComparer.Ordinal)); return; }
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<object>();
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<object>(); var scriptStarts = new Dictionary<string, double>(); var scriptDurations = new List<double>();
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<object>();
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<object>();
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)));
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";
if (args.Contains("--loading", StringComparer.Ordinal)) { LoadingProfile.Run(root, label); return; }
if (args.Contains("--monaco", StringComparer.Ordinal)) { MonacoProfile.Run(root, label, args.Contains("--regex", StringComparer.Ordinal)); return; }
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<object>();
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<object>(); var scriptStarts = new Dictionary<string, double>(); var scriptDurations = new List<double>();
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<object>();
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<object>();
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)));