Ely Office: six offline-first desktop apps in Rust and GPUI

ElyDoc, ElyTable, ElyShow (LibreOfficeKit), ElyPDF (MuPDF), ElyTree (Git),
ElyMark (Milkdown and CodeMirror in CEF). ElyMail and ElyAgent follow.
This commit is contained in:
ZacharyZhang-NY committed 2026-10-08 21:20:08 -04:00
commit b7fca0146d
817 files changed
+143101

No files matched your search

Binary file not shown.
Binary file not shown.
Binary file not shown.
+391
View File
@@ -0,0 +1,391 @@
#!/usr/bin/env python3
"""PERF-01 to PERF-08 on release builds, written as a record."""
import argparse
import datetime
import hashlib
import json
import os
import platform
import re
import shutil
import subprocess
import sys
import tempfile
import time
import samples
ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
APPS = {"ely-doc": "ElyDoc", "ely-table": "ElyTable", "ely-show": "ElyShow",
"ely-pdf": "ElyPDF", "ely-tree": "ElyTree", "ely-mark": "ElyMark"}
TARGETS = {"PERF-01": 3000, "PERF-02": 100, "PERF-03": 5000, "PERF-04": 5000,
"PERF-05": 3000, "PERF-06": 1000, "PERF-07": 2000}
def sh(*args, **kw):
return subprocess.run(args, check=True, capture_output=True, text=True, **kw).stdout
def tools(folder):
for tool in ["ax-values", "ax-press", "click"]:
sh("swiftc", "-O", "-o", os.path.join(folder, tool), os.path.join(ROOT, "tests", "scripts", tool + ".swift"))
class App:
"""A release app run, timed from spawn, read via log."""
def __init__(self, slug, home, args=(), smoke="hold"):
self.slug, self.home, self.tools = slug, home, os.path.dirname(home)
self.log = os.path.join(home, "Library", "Application Support", "ElyOffice", slug, "ely.log")
# Starts may trim the log; each run starts fresh.
if os.path.exists(self.log):
os.remove(self.log)
self.seen = 0
env = dict(os.environ, HOME=home, ELY_SMOKE=smoke, ELY_LOCALE="en", ELY_NO_DIALOG="1",
ELY_LOG="debug", ELY_STALLS="1")
self.start = time.monotonic()
self.process = subprocess.Popen([os.path.join(ROOT, "target", "release", slug), *args], env=env,
stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, text=True)
def lines(self):
if not os.path.exists(self.log):
return []
with open(self.log, errors="replace") as log:
log.seek(self.seen)
return log.read().splitlines()
def wait(self, pattern, after=None, timeout=60):
"""Milliseconds from spawn until a line matches, past `after`."""
called = time.monotonic()
while time.monotonic() - called < timeout:
lines = self.lines()
if after is not None:
at = next((i for i, line in enumerate(lines) if re.search(after, line)), None)
lines = [] if at is None else lines[at + 1:]
if any(re.search(pattern, line) for line in lines):
return (time.monotonic() - self.start) * 1000
time.sleep(0.01)
tail = "\n".join(self.lines()[-12:])
raise RuntimeError(f"{self.slug}: no {pattern!r} in {timeout} s; the log ends:\n{tail}")
def ready(self, timeout=60):
"""ELY_SMOKE=1: milliseconds until the first frame."""
out, _ = self.process.communicate(timeout=timeout)
took = (time.monotonic() - self.start) * 1000
if f"ready {APPS[self.slug]}" not in out:
raise RuntimeError(f"{self.slug} did not report ready: {out!r}")
# Its engine may still be starting; the next run waits.
for _ in range(300):
if subprocess.run(["pgrep", "-f", self.home], capture_output=True).returncode != 0:
break
time.sleep(0.05)
return took
def family(self):
out = sh("ps", "-A", "-o", "pid=,ppid=,rss=")
rows = [tuple(map(int, line.split())) for line in out.splitlines()]
ours, grew = {self.process.pid}, True
while grew:
grew = False
for pid, ppid, _ in rows:
if ppid in ours and pid not in ours:
ours.add(pid)
grew = True
return ours, sum(rss for pid, _, rss in rows if pid in ours)
def memory_mb(self):
return self.family()[1] / 1024
def files(self):
ours, _ = self.family()
return sum(len(sh("lsof", "-p", str(pid)).splitlines()) for pid in ours)
def front(self):
sh("osascript", "-e", f'tell application "System Events" to set frontmost of (first process whose unix id is {self.process.pid}) to true')
time.sleep(0.5)
def ax(self):
return sh(os.path.join(self.tools, "ax-values"), str(self.process.pid))
def stop(self):
ours, _ = self.family()
for pid in ours:
try:
os.kill(pid, 9)
except ProcessLookupError:
pass
self.process.wait()
def keys(script):
sh("osascript", "-e", 'tell application "System Events"', "-e", script, "-e", "end tell")
def percentile(values, p):
ordered = sorted(values)
return ordered[min(len(ordered) - 1, int(len(ordered) * p))]
# A page that failed to paint voids the run.
PAINT_FAILED = r"lok: tile \(.*\) failed|pdf: drawing a page failed"
def opened(slug, home, sample, marks, runs):
"""Spawn with a file until `marks` all appear, in order."""
times, memory = [], []
for _ in range(runs):
app = App(slug, home, [sample])
try:
after = None
for mark in marks:
took = app.wait(mark, after)
after = mark
failed = [line for line in app.lines() if re.search(PAINT_FAILED, line)]
if failed:
raise RuntimeError(f"{slug}: {sample} did not paint: {failed[0]}")
times.append(took)
memory.append(app.memory_mb())
finally:
app.stop()
return times, memory
def laid_out(work, sample, pages):
"""The kernel lays `sample` out to exactly `pages` pages."""
app = App("ely-doc", os.path.join(work, "home-pages"), [sample])
try:
app.wait(rf"lok: {pages} pages$", timeout=120)
time.sleep(3)
counts = [line for line in app.lines() if re.search(r"lok: \d+ pages$", line)]
if not counts[-1].endswith(f" {pages} pages"):
raise RuntimeError(f"{sample} lays out as {counts[-1]}, not {pages} pages")
finally:
app.stop()
def perf01(work, runs):
rows = []
for slug in APPS:
home = os.path.join(work, "home-" + slug)
first = App(slug, home, smoke="1").ready()
daily = [App(slug, home, smoke="1").ready() for _ in range(runs)]
rows.append((slug, first, daily))
return rows
def perf02(work, runs):
"""Key to new tile in ElyDoc, and main-thread stalls meanwhile."""
app = App("ely-doc", os.path.join(work, "home-typing"))
try:
app.wait("opened Untitled")
app.front()
tick = r"lok: edit to tile (\d+) ms"
for n in range(runs):
keys('keystroke "x"')
# Each key must show; a lost one fails the run.
for _ in range(500):
if sum(1 for line in app.lines() if re.search(tick, line)) > n:
break
time.sleep(0.01)
else:
raise RuntimeError(f"ely-doc: key {n + 1} of {runs} never reached a tile")
time.sleep(0.15)
lines = app.lines()
finally:
app.stop()
ticks = [int(m.group(1)) for line in lines if (m := re.search(tick, line))]
if len(ticks) != runs:
raise RuntimeError(f"ely-doc: {runs} keys gave {len(ticks)} timings")
stalls = [int(m.group(1)) for line in lines if (m := re.search(r"main thread was busy (\d+) ms", line))]
return ticks, stalls
def perf06(work, repo, runs):
"""History first screen, then a common query's first results."""
home = os.path.join(work, "home-ely-tree")
first, found = [], []
for _ in range(runs):
app = App("ely-tree", home, [repo])
try:
app.wait("tree: opened")
app.front()
pressed = (time.monotonic() - app.start) * 1000
sh(os.path.join(work, "ax-press"), str(app.process.pid), "History")
first.append(app.wait(r"history shows \d+ commits, searched false") - pressed)
field = next(line for line in app.ax().splitlines() if line.startswith("AXTextField"))
x, y = field.rsplit("at=", 1)[1].split(",")
sh(os.path.join(work, "click"), x, y)
keys('keystroke "part 42"')
asked = (time.monotonic() - app.start) * 1000
keys("key code 36")
found.append(app.wait(r"searched true") - asked)
finally:
app.stop()
return first, found
def raise_main(app):
"""The document window as key, once a dialog closed."""
sh("osascript", "-e", f'tell application "System Events" to tell (first process whose unix id is {app.process.pid}) '
f'to perform action "AXRaise" of window "{APPS[app.slug]}"')
time.sleep(0.3)
def perf08(work, cycles=40):
"""New and close a document; memory, handles and live documents."""
app = App("ely-doc", os.path.join(work, "home-cycles"))
docs = r"lok-worker: (\d+) documents open"
def open_docs():
counts = [int(m.group(1)) for line in app.lines() if (m := re.search(docs, line))]
return counts[-1] if counts else None
try:
app.wait("opened Untitled")
app.front()
time.sleep(2)
baseline = (app.memory_mb(), app.files(), open_docs())
seen = []
for n in range(cycles):
keys('keystroke "n" using command down')
for _ in range(40):
if "title=Use template" in app.ax():
break
time.sleep(0.25)
sh(os.path.join(work, "ax-press"), str(app.process.pid), "Use template", "AXButton")
app.wait(f"opened Untitled {n + 2} ", timeout=30)
time.sleep(1)
# A keystroke can miss a window still taking focus.
for _ in range(3):
app.front()
raise_main(app)
keys('keystroke "w" using command down')
try:
app.wait(f"office: closed Untitled {n + 2}$", timeout=5)
break
except RuntimeError:
# A keystroke that lost its Command edits the document.
if "title=Don't Save" in app.ax():
raise RuntimeError(f"ely-doc: Untitled {n + 2} was edited by a stray key; it asks to save")
continue
else:
said = [line for line in app.lines() if re.search("clos|dialog|opened|focus", line)]
raise RuntimeError(f"ely-doc: Untitled {n + 2} never closed: {said[-8:]}")
# The kernel lets go in the background.
for _ in range(100):
if open_docs() == baseline[2]:
break
time.sleep(0.05)
time.sleep(0.5)
seen.append((app.memory_mb(), app.files(), open_docs()))
finally:
app.stop()
# The first dialog loads system fonts and AppKit tables once.
first, last = seen[0], seen[-1]
if last[1] != first[1] or last[2] != baseline[2]:
raise RuntimeError(f"ely-doc: after {cycles} cycles {last[1:]} files and documents, "
f"not {first[1]} and {baseline[2]}")
return baseline, seen
def environment(found):
gpu = re.findall(r"Chipset Model: (.+)", sh("system_profiler", "SPDisplaysDataType"))
fonts = sum(len(files) for folder in ["/System/Library/Fonts", "/Library/Fonts"] for _, _, files in os.walk(folder))
builds = {slug: hashlib.sha256(open(os.path.join(ROOT, "target", "release", slug), "rb").read()).hexdigest()[:16]
for slug in APPS}
return {
"date": datetime.date.today().isoformat(),
"machine": sh("sysctl", "-n", "hw.model").strip(),
"cpu": sh("sysctl", "-n", "machdep.cpu.brand_string").strip(),
"cores": sh("sysctl", "-n", "hw.ncpu").strip(),
"memory_gb": int(sh("sysctl", "-n", "hw.memsize")) // 2**30,
"gpu": ", ".join(gpu),
"system": f"macOS {platform.mac_ver()[0]}",
"fonts": fonts,
"builds": builds,
"samples": {name: value[1] for name, value in found.items()},
}
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--runs", type=int, default=30)
parser.add_argument("--only", nargs="*")
parser.add_argument("--out", default=os.path.join(ROOT, "docs", "perf"))
args = parser.parse_args()
want = lambda perf: not args.only or perf in args.only
if sys.platform != "darwin":
sys.exit("SKIP: the bench drives macOS windows")
sh("cargo", "build", "--release", "-q", "--bins", "-p", "ely-lok", "-p", "ely-pdf-engine",
*[arg for slug in APPS for arg in ("-p", slug)], cwd=ROOT)
git = os.path.join(ROOT, "engines", "git", "bin", "git")
found = samples.made(os.path.join(ROOT, "engines", "bench"), git)
work = tempfile.mkdtemp()
tools(work)
rows = []
try:
if want("PERF-01"):
for slug, first, daily in perf01(work, args.runs):
rows.append(("PERF-01", f"{slug} first run", [first], None))
rows.append(("PERF-01", f"{slug} daily start", daily, None))
# The window opens first; the blank document follows the engine.
for slug in ["ely-doc", "ely-table", "ely-show"]:
blank = []
for _ in range(args.runs):
app = App(slug, os.path.join(work, "home-" + slug))
try:
blank.append(app.wait("opened Untitled"))
finally:
app.stop()
rows.append(("PERF-01", f"{slug} blank document ready", blank, None))
if want("PERF-02"):
ticks, stalls = perf02(work, args.runs)
rows.append(("PERF-02", "ElyDoc key to new tile", ticks, None))
rows.append(("PERF-02", f"main thread busy >100 ms: {len(stalls)} times", stalls or [0], None))
cases = [("PERF-03", "ely-doc", "doc.docx", ["opened doc.docx", r"lok: tile \(\d+, \d+\) shown"]),
("PERF-04", "ely-table", "sheet.xlsx", ["opened sheet.xlsx", r"lok: tile \(\d+, \d+\) shown"]),
("PERF-05", "ely-pdf", "pages.pdf", ["opened pages.pdf", r"pdf: a tile of page 0 drawn"]),
("PERF-07", "ely-mark", "notes.md", ["opened notes.md", r"mark: shown as \"visual\""])]
if want("PERF-03"):
laid_out(work, found["doc.docx"][0], 100)
for perf, slug, name, marks in cases:
if want(perf):
times, memory = opened(slug, os.path.join(work, "home-" + slug), found[name][0], marks, args.runs)
rows.append((perf, f"{APPS[slug]} {name}", times, max(memory)))
if want("PERF-06"):
first, searched = perf06(work, found["repo"][0], args.runs)
rows.append(("PERF-06", "ElyTree history, press to first screen", first, None))
rows.append(("PERF-06", "ElyTree query 'part 42', first results", searched, None))
held = perf08(work) if want("PERF-08") else None
finally:
shutil.rmtree(work, ignore_errors=True)
write(args.out, environment(found), rows, held, args.runs)
def write(out, env, rows, held, runs):
os.makedirs(out, exist_ok=True)
path = os.path.join(out, f"{env['date']}-{env['machine']}.md")
lines = [f"# Benchmark {env['date']}", "", "Format: docs/perf/README.md.", "", "```json",
json.dumps(env, indent=2), "```", "",
"| PERF | Case | Runs | P50 ms | P95 ms | Target ms | Total memory MB |", "| --- | --- | --- | --- | --- | --- | --- |"]
for perf, case, times, memory in rows:
target = TARGETS.get(perf)
p95 = percentile(times, 0.95)
verdict = "" if target is None or "busy" in case else (" ✓" if p95 <= target else " ✗")
mem = "" if memory is None else f"{memory:.0f}"
lines.append(f"| {perf} | {case} | {len(times)} | {percentile(times, 0.5):.0f} | {p95:.0f}{verdict} | "
f"{target or ''} | {mem} |")
if held:
(memory, files, docs), seen = held
lines += ["", f"PERF-08, ElyDoc creating and closing a document {len(seen)} times "
"(total memory MB / open files / kernel documents):", "",
f"before: {memory:.0f}/{files}/{docs}", "",
" ".join(f"{memory:.0f}/{files}/{docs}" for memory, files, docs in seen)]
lines += ["", "PERF-09 (Agent sessions) belongs to the second release.", ""]
with open(path, "w") as record:
record.write("\n".join(lines))
print(path)
if __name__ == "__main__":
main()
+171
View File
@@ -0,0 +1,171 @@
"""Benchmark samples, made the same way every time (§20.1)."""
import hashlib
import os
import random
import struct
import subprocess
import zipfile
import zlib
def noise_png(seed, width, height):
"""An RGB PNG of seeded noise; it does not compress."""
rng = random.Random(seed)
rows = b"".join(b"\0" + rng.randbytes(width * 3) for _ in range(height))
def chunk(kind, body):
return struct.pack(">I", len(body)) + kind + body + struct.pack(">I", zlib.crc32(kind + body))
head = struct.pack(">IIBBBBB", width, height, 8, 2, 0, 0, 0)
return b"\x89PNG\r\n\x1a\n" + chunk(b"IHDR", head) + chunk(b"IDAT", zlib.compress(rows, 1)) + chunk(b"IEND", b"")
W = 'xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" ' \
'xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" ' \
'xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing" ' \
'xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" ' \
'xmlns:pic="http://schemas.openxmlformats.org/drawingml/2006/picture"'
def picture(n):
cx, cy = 3600000, 2700000
return (f'<w:p><w:r><w:drawing><wp:inline><wp:extent cx="{cx}" cy="{cy}"/><wp:docPr id="{n}" name="p{n}"/>'
'<a:graphic><a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/picture">'
f'<pic:pic><pic:nvPicPr><pic:cNvPr id="{n}" name="p{n}.png"/><pic:cNvPicPr/></pic:nvPicPr>'
f'<pic:blipFill><a:blip r:embed="img{n}"/><a:stretch><a:fillRect/></a:stretch></pic:blipFill>'
f'<pic:spPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="{cx}" cy="{cy}"/></a:xfrm>'
'<a:prstGeom prst="rect"><a:avLst/></a:prstGeom></pic:spPr></pic:pic></a:graphicData></a:graphic>'
'</wp:inline></w:drawing></w:r></w:p>')
def docx(path):
"""PERF-03: 100 pages, 20 images, under 10 MB."""
rng = random.Random(3)
words = "office local file engine page table slide note draft review".split()
body = []
for page in range(100):
# The break runs in the heading; Heading 1 drops pageBreakBefore.
breaks = '<w:r><w:br w:type="page"/></w:r>' if page else ""
body.append(f'<w:p><w:pPr><w:pStyle w:val="Heading1"/></w:pPr>{breaks}<w:r><w:t>Page {page + 1}</w:t></w:r></w:p>')
for _ in range(6):
text = " ".join(rng.choice(words) for _ in range(60))
body.append(f"<w:p><w:r><w:t>{text}</w:t></w:r></w:p>")
if page % 5 == 0:
body.append(picture(page // 5 + 1))
rels = "".join(f'<Relationship Id="img{n}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" '
f'Target="media/p{n}.png"/>' for n in range(1, 21))
with zipfile.ZipFile(path, "w", zipfile.ZIP_DEFLATED) as archive:
archive.writestr("[Content_Types].xml", '<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">'
'<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>'
'<Default Extension="xml" ContentType="application/xml"/><Default Extension="png" ContentType="image/png"/>'
'<Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/></Types>')
archive.writestr("_rels/.rels", '<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">'
'<Relationship Id="r1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/></Relationships>')
archive.writestr("word/_rels/document.xml.rels", f'<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">{rels}</Relationships>')
archive.writestr("word/document.xml", f'<w:document {W}><w:body>{"".join(body)}</w:body></w:document>')
for n in range(1, 21):
archive.writestr(f"word/media/p{n}.png", noise_png(n, 400, 300), zipfile.ZIP_STORED)
def xlsx(path):
"""PERF-04: 100,000 values and 10,000 simple formulas."""
rows = []
for r in range(1, 10001):
cells = "".join(f'<c r="{chr(65 + c)}{r}"><v>{(r * 7 + c * 13) % 1000}</v></c>' for c in range(10))
rows.append(f'<row r="{r}">{cells}<c r="K{r}"><f>SUM(A{r}:J{r})</f></c></row>')
ns = 'xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" ' \
'xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"'
with zipfile.ZipFile(path, "w", zipfile.ZIP_DEFLATED) as archive:
archive.writestr("[Content_Types].xml", '<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">'
'<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>'
'<Default Extension="xml" ContentType="application/xml"/>'
'<Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>'
'<Override PartName="/xl/worksheets/sheet1.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/></Types>')
archive.writestr("_rels/.rels", '<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">'
'<Relationship Id="r1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/></Relationships>')
archive.writestr("xl/_rels/workbook.xml.rels", '<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">'
'<Relationship Id="s1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet1.xml"/></Relationships>')
archive.writestr("xl/workbook.xml", f'<workbook {ns}><sheets><sheet name="Data" sheetId="1" r:id="s1"/></sheets></workbook>')
archive.writestr("xl/worksheets/sheet1.xml", f'<worksheet {ns}><sheetData>{"".join(rows)}</sheetData></worksheet>')
def pdf(path):
"""PERF-05: 1000 pages, about 200 MB, one image a page."""
rng = random.Random(5)
offsets, out = [], open(path, "wb")
out.write(b"%PDF-1.7\n")
def obj(n, body):
offsets.append((n, out.tell()))
out.write(f"{n} 0 obj\n".encode() + body + b"\nendobj\n")
pages = 1000
kids = " ".join(f"{3 + i * 3} 0 R" for i in range(pages))
obj(1, b"<< /Type /Catalog /Pages 2 0 R >>")
obj(2, f"<< /Type /Pages /Kids [{kids}] /Count {pages} >>".encode())
for i in range(pages):
page, content, image = 3 + i * 3, 4 + i * 3, 5 + i * 3
pixels = rng.randbytes(240 * 280 * 3)
obj(page, f"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Contents {content} 0 R "
f"/Resources << /XObject << /I {image} 0 R >> /Font << /F <</Type /Font /Subtype /Type1 /BaseFont /Helvetica>> >> >> >>".encode())
draw = f"q 480 0 0 560 66 160 cm /I Do Q BT /F 24 Tf 72 100 Td (Page {i + 1}) Tj ET".encode()
obj(content, f"<< /Length {len(draw)} >>\nstream\n".encode() + draw + b"\nendstream")
obj(image, f"<< /Type /XObject /Subtype /Image /Width 240 /Height 280 /ColorSpace /DeviceRGB "
f"/BitsPerComponent 8 /Length {len(pixels)} >>\nstream\n".encode() + pixels + b"\nendstream")
xref = out.tell()
count = 3 + pages * 3
places = dict(offsets)
out.write(f"xref\n0 {count}\n0000000000 65535 f \n".encode())
for n in range(1, count):
out.write(f"{places[n]:010d} 00000 n \n".encode())
out.write(f"trailer\n<< /Size {count} /Root 1 0 R >>\nstartxref\n{xref}\n%%EOF\n".encode())
out.close()
def repo(path, git):
"""PERF-06: a repository of 100,000 commits."""
subprocess.run([git, "init", "-q", "-b", "main", path], check=True)
stream = []
for n in range(1, 100001):
message = f"Change {n}: {'fix' if n % 3 else 'add'} part {n % 97}\n".encode()
body = f"line {n}\n".encode()
stream.append(b"commit refs/heads/main\n")
stream.append(f"committer Bench <bench@ely.local> {1700000000 + n} +0000\n".encode())
stream.append(f"data {len(message)}\n".encode() + message)
stream.append(f"M 644 inline part{n % 97}.txt\ndata {len(body)}\n".encode() + body)
subprocess.run([git, "-C", path, "fast-import", "--quiet"], input=b"".join(stream), check=True)
subprocess.run([git, "-C", path, "checkout", "-q", "main"], check=True)
def markdown(path):
"""PERF-07: a 1 MB Markdown document."""
rng = random.Random(7)
words = "local file engine page table slide note draft review commit".split()
parts, size, n = [], 0, 0
while size < 1024 * 1024:
n += 1
block = f"## Section {n}\n\n" + " ".join(rng.choice(words) for _ in range(120)) + \
f"\n\n- item {n}\n- **bold** and `code`\n\n```rust\nfn f{n}() -> u32 {{ {n} }}\n```\n\n"
parts.append(block)
size += len(block)
with open(path, "w") as out:
out.write("".join(parts))
def made(folder, git):
"""Each sample, made once, with its SHA-256."""
os.makedirs(folder, exist_ok=True)
makers = {"doc.docx": docx, "sheet.xlsx": xlsx, "pages.pdf": pdf, "notes.md": markdown}
found = {}
for name, make in makers.items():
path = os.path.join(folder, name)
if not os.path.exists(path):
make(path)
found[name] = (path, hashlib.sha256(open(path, "rb").read()).hexdigest())
path = os.path.join(folder, "repo")
if not os.path.exists(path):
repo(path, git)
head = subprocess.run([git, "-C", path, "rev-parse", "HEAD"], capture_output=True, text=True, check=True).stdout.strip()
found["repo"] = (path, head)
return found
+104
View File
@@ -0,0 +1,104 @@
#!/usr/bin/env python3
"""Long editing and switching in ElyDoc, checked as it goes."""
import argparse
import os
import shutil
import sys
import tempfile
import time
import zipfile
import xml.etree.ElementTree as ET
import bench
W = "{http://schemas.openxmlformats.org/wordprocessingml/2006/main}"
def words(path):
"""The saved text, read without LibreOffice; a broken file raises."""
with zipfile.ZipFile(path) as archive:
body = ET.fromstring(archive.read("word/document.xml"))
# Runs join without spaces; a word may span two.
paragraphs = ("".join(node.text or "" for node in p.iter(W + "t")) for p in body.iter(W + "p"))
return " ".join(paragraphs).split()
def check(paths, n):
"""What the saved files get wrong after `n` cycles."""
a, b = words(paths["a.docx"]), words(paths["b.docx"])
problems = [f"a lacks A{k}" for k in range(1, n + 1) if f"A{k}" not in a]
problems += [f"b keeps B{k}" for k in range(1, n + 1) if f"B{k}" in b]
if any(word.startswith("A") and word[1:].isdigit() for word in b):
problems.append("b holds a's text")
return problems
def saves(app):
"""The app's last save lines, for a failure."""
return [line[-90:] for line in app.lines() if "save" in line][-6:]
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--minutes", type=float, default=480)
args = parser.parse_args()
if sys.platform != "darwin":
sys.exit("SKIP: the soak drives macOS windows")
work = tempfile.mkdtemp()
bench.tools(work)
fixture = os.path.join(bench.ROOT, "tests", "fixtures", "office", "t.docx")
paths = {name: os.path.join(work, name) for name in ["a.docx", "b.docx"]}
for path in paths.values():
shutil.copy(fixture, path)
app = bench.App("ely-doc", os.path.join(work, "home"), list(paths.values()))
def save():
"""Saves through the menu: a keystroke can be lost."""
bench.sh("osascript", "-e", f'tell application "System Events" to tell (first process whose unix id is {app.process.pid}) '
'to click menu item "Save" of menu "File" of menu bar 1')
def press(title):
"""Chooses a tab, then puts the keyboard in its document."""
bench.sh(os.path.join(work, "ax-press"), str(app.process.pid), title, "AXRadioButton")
time.sleep(0.5)
area = next(line for line in app.ax().splitlines() if line.startswith("AXTextArea"))
x, y = area.rsplit("at=", 1)[1].split(",")
bench.sh(os.path.join(work, "click"), x, y)
# The click may land mid-word; typing goes at the end.
bench.keys("key code 119 using command down")
time.sleep(0.3)
try:
app.wait("opened b.docx")
app.front()
end, n, memory = time.monotonic() + args.minutes * 60, 0, []
while time.monotonic() < end:
n += 1
press("a.docx")
bench.keys(f'keystroke " A{n} "')
save()
press("b.docx")
bench.keys(f'keystroke "B{n}"')
time.sleep(0.3)
bench.keys('keystroke "z" using command down')
save()
time.sleep(0.5)
if n % 10 == 0:
# Saves finish in their own time; give them ten seconds.
for _ in range(40):
problems = check(paths, n)
if not problems:
break
time.sleep(0.25)
if problems:
raise RuntimeError(f"cycle {n}: {problems[:5]}; a ends {words(paths['a.docx'])[-8:]}; log: {saves(app)}")
memory.append(app.memory_mb())
print(f"cycle {n}: ok, {memory[-1]:.0f} MB", flush=True)
finally:
app.stop()
shutil.rmtree(work, ignore_errors=True)
print(f"ok: {n} cycles in {args.minutes} min; memory MB {memory[0]:.0f} → {memory[-1]:.0f}")
if __name__ == "__main__":
main()