Files
mail/scripts/benchmark-sidebar.py

121 lines
6.5 KiB
Python

#!/usr/bin/env python3
"""Render synthetic folder trees and count constructed sidebar delegates.
Requires slint-viewer 1.18+. Use --baseline-ref with a pre-virtualization
commit to compare the original ScrollView. Timings include interpretation, layout, and one software frame;
they are diagnostic, not a compiled application or hardware benchmark.
"""
import argparse
import json
from pathlib import Path
import re
import statistics
import subprocess
import tempfile
import time
REPO = Path(__file__).resolve().parents[1]
def mailbox(account_id, folder=None):
return {
"account_id": account_id, "folder_id": folder if folder is not None else -1,
"parent_folder_id": -1, "depth": 0, "has_children": False, "expanded": True,
"is_standard": False, "label_has_emoji": False,
"label": f"Folder {folder}" if folder is not None else f"Account {account_id}",
"scope": f"Folder:{folder}" if folder is not None else f"Account:{account_id}",
"context": f"Account:{account_id}", "detail": "", "avatar": "A",
"has_avatar": False, "is_account": folder is None, "count": "",
"color": "#000000", "has_custom_color": False,
}
def fixture(account_count, folder_count, flat):
accounts = [mailbox(i) for i in range(account_count)]
folders = [mailbox(i, i * folder_count + f) for i in range(account_count) for f in range(folder_count)]
if not flat:
return {"account-mailboxes": accounts, "mailboxes": accounts + folders}
label = dict(id=-1, name="", name_has_emoji=False, color="#000000", applied=False, is_auto=False)
def row(kind, key, data=None, opened=False):
return dict(kind=kind, key=key, mailbox=data or mailbox(-1), label=label, open=opened)
rows = [row("unified-heading", "heading"), row("unified-section", "unified", opened=True),
row("unified-inbox", "inbox"), row("categories-section", "categories"),
row("accounts-heading", "accounts")]
for account in accounts:
aid = account["account_id"]
rows.append(row("account", f"account:{aid}", account, True))
rows.extend(row("folder", f"folder:{f['folder_id']}", f) for f in folders if f["account_id"] == aid)
rows.append(row("new-folder", f"new:{aid}", account))
return {"rows": rows}
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--baseline-ref")
parser.add_argument("--rounds", type=int, default=3)
parser.add_argument("--output-dir", type=Path, default=Path("/tmp/flectar-sidebar-benchmark"))
args = parser.parse_args()
args.output_dir.mkdir(parents=True, exist_ok=True)
path = "ui/components/sidebar.slint"
source = subprocess.check_output(["git", "show", f"{args.baseline_ref}:{path}"], cwd=REPO, text=True) if args.baseline_ref else (REPO / path).read_text()
flat = "[SidebarRow]" in source
for component in ("SidebarItem", "SidebarSectionHeader"):
source = source.replace(f"export component {component} inherits Rectangle {{",
f'export component {component} inherits Rectangle {{\n init => {{ debug("sidebar-delegate-created"); }}')
source = re.sub(r'"((?:\.\./|\./)[^"\n]+)"', lambda m: json.dumps(str((REPO / "ui/components" / m[1]).resolve())), source)
with tempfile.TemporaryDirectory(prefix="sidebar-bench-") as directory:
directory = Path(directory)
if not args.baseline_ref and (REPO / "ui/components/sidebar-controls.slint").exists():
controls = (REPO / "ui/components/sidebar-controls.slint").read_text()
for component in ("SidebarItem", "SidebarSectionHeader"):
controls = controls.replace(f"export component {component} inherits SidebarRowSurface {{",
f'export component {component} inherits SidebarRowSurface {{\n init => {{ debug("sidebar-delegate-created"); }}')
controls = re.sub(r'"((?:\.\./|\./)[^"\n]+)"', lambda m: json.dumps(str((REPO / "ui/components" / m[1]).resolve())), controls)
(directory / "sidebar-controls.slint").write_text(controls)
(directory / "sidebar.slint").write_text(source)
model_type = "SidebarRow" if flat else "MailboxRow"
account_property = "" if flat else "in property <[MailboxRow]> account-mailboxes;"
binding = ("rows: root.rows;" if flat else
"mailboxes: root.rows; account-mailboxes: root.account-mailboxes;")
(directory / "main.slint").write_text(f'''import {{ {model_type} }} from "{REPO / 'ui/models.slint'}";
import {{ MailSidebar }} from "sidebar.slint";
export component Harness inherits Window {{
width: 240px; height: 800px;
in property <[{model_type}]> rows;
{account_property}
MailSidebar {{
{binding}
background-color: #f4f4f4; body-color: #222; title-color: #222;
muted-color: #777; accent-color: #06e;
selection-background: #dfecff; hover-background: #ddd;
}}
}}
''')
for account_count, folder_count in [(1, 750), (5, 150), (5, 1500)]:
data = fixture(account_count, folder_count, flat)
if not flat:
data["rows"] = data.pop("mailboxes")
(directory / "data.json").write_text(json.dumps(data))
times, counts = [], []
output = args.output_dir / f"{'flat' if flat else 'baseline'}-{account_count}x{folder_count}.png"
for _ in range(args.rounds):
started = time.perf_counter()
result = subprocess.run(["slint-viewer", "-I", str(REPO / "ui/components"), "--style", "fluent", "--load-data", str(directory / "data.json"),
"--screenshot", str(output), str(directory / "main.slint")], capture_output=True, text=True)
if result.returncode:
raise RuntimeError(result.stdout + result.stderr)
times.append(time.perf_counter() - started)
counts.append((result.stdout + result.stderr).count("sidebar-delegate-created"))
if "Warning:" in result.stderr:
raise RuntimeError(result.stderr)
assert min(counts) > 0, "instrumentation must observe instantiated delegates"
if flat:
assert max(counts) < 100, f"virtualization regression: {counts} delegates"
print(f"accounts={account_count} folders_each={folder_count} delegates={counts} median_seconds={statistics.median(times):.4f} screenshot={output}", flush=True)
if __name__ == "__main__":
main()