using System.Diagnostics; using System.Text.Json.Nodes; using decePubClient.E2E.Support; using Microsoft.Playwright; namespace decePubClient.E2E.Perf { // The home feed with thousands of posts behind it: load0's home, filled by SocialPub's load runs // (tools/pasture/load.sh, the flood peer). Scrolls it until 100, 500, 1000 and 2000 posts are loaded (the feed renders // only the cards near the view, so it counts what it holds, data-feed-count) and measures, at each, how long the // pages took, the page's size (DOM nodes, JS and .NET heaps) and the long tasks; and the time to the first card. Writes perf.json next to the test output; nothing is asserted beyond reaching the cards, the numbers are // for comparing runs. [Trait("Category", "Perf")] public sealed class FeedPerfTests(Browser browser) { static readonly int[] Steps = [100, 500, 1000, 2000]; [Fact] public async Task A_long_home_feed_scrolls_to_two_thousand_cards() { var persona = new Pasture.Persona("load0", "load0", "pastureroot", "Pasture-Pass-1!", null, null, null); await using var session = await SignIn.As(browser, persona, fresh: true); var page = session.Page; await page.EvaluateAsync("() => localStorage.removeItem('FeedFilter')"); await page.AddInitScriptAsync(@"window.__longTasks = []; new PerformanceObserver(list => list.getEntries().forEach(e => window.__longTasks.push(e.duration))).observe({ type: 'longtask', buffered: true });"); var clock = Stopwatch.StartNew(); await page.GotoAsync($"{Pasture.Site}/"); var cards = page.Locator("article[data-uri]"); await cards.First.WaitForAsync(new() { Timeout = 60000 }); var firstCard = clock.Elapsed.TotalMilliseconds; var loaded = page.Locator("[data-feed-count]"); async Task Loaded() => int.Parse(await loaded.GetAttributeAsync("data-feed-count") ?? "0"); Assert.SkipWhen(await Loaded() < 20, "load0's home is short: run SocialPub's tools/pasture/load.sh first"); var report = new JsonObject { ["firstCardMs"] = Math.Round(firstCard), ["steps"] = new JsonArray() }; foreach (var target in Steps) { var step = Stopwatch.StartNew(); var count = await Loaded(); var stalled = Stopwatch.StartNew(); while (count < target && stalled.Elapsed < TimeSpan.FromSeconds(20)) { await page.EvaluateAsync("() => { const s = document.getElementById('feed-scroll'); s.scrollTop = s.scrollHeight; }"); await page.WaitForTimeoutAsync(150); var now = await Loaded(); if (now > count) stalled.Restart(); count = now; } var metrics = await page.EvaluateAsync(@"() => { const heap = performance.memory ? performance.memory.usedJSHeapSize : null; const runtime = globalThis.getDotnetRuntime ? globalThis.getDotnetRuntime(0) : null; const wasm = runtime && runtime.Module && runtime.Module.HEAPU8 ? runtime.Module.HEAPU8.length : null; const tasks = window.__longTasks.splice(0); return { json: JSON.stringify({ dom: document.getElementsByTagName('*').length, jsHeap: heap, wasmHeap: wasm, longTasks: tasks.length, longTaskMs: Math.round(tasks.reduce((a, b) => a + b, 0)) }) }; }"); var row = JsonNode.Parse(metrics.Json)!.AsObject(); row["target"] = target; row["posts"] = count; row["cards"] = await cards.CountAsync(); row["seconds"] = Math.Round(step.Elapsed.TotalSeconds, 1); report["steps"]!.AsArray().Add(row); if (count < target) break; } var path = Path.Combine(AppContext.BaseDirectory, "perf.json"); await File.WriteAllTextAsync(path, report.ToJsonString(new() { WriteIndented = true })); TestContext.Current.SendDiagnosticMessage(report.ToJsonString()); Assert.True(report["steps"]!.AsArray().Count > 0); await session.AssertGuards(); } public sealed class JsonElementHolder { public string Json { get; set; } } } }