83 lines
3.8 KiB
C#
83 lines
3.8 KiB
C#
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<int> 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<JsonElementHolder>(@"() => {
|
||
|
|
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; }
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|