Files
openswarm/docs/perf/winv2/measure_appbuilder.py
T
Eric 24a767eec9 [eric] perf: winv2 windows startup + app builder profiling, tracking doc, graphs
- warm boot floor 8256ms -> 857ms; app builder download + create breakdown measured

- harnesses, csv, dependency-free svg graphs, per-step savings table, #9 plan
2026-06-16 21:58:39 -07:00

136 lines
5.1 KiB
Python

"""Granular App Builder first-app create/"download" profiler (winv2 Task #3).
Incremental + bounded: each phase appends to appbuilder_breakdown.csv and flushes
the instant it finishes, so a slow/hung later phase can't erase earlier numbers.
Run UNBUFFERED (python -u) so progress is visible mid-run. Cheap phases first.
Phases:
1. seed workspace + link node_modules (per-app cost, uses real warm cache)
2. download: npm install (cold, no archive) (the "slow as bricks" download)
3. download: archive extract (new build path) (tar the just-installed nm, time extract)
4. vite bind: cold vite cache (first app ever)
5. vite bind: warm shared cache (subsequent apps)
Isolated temp dirs; never mutates the user's real caches (read-only link to the
warm node_modules cache; vite cache is overridden to temp for the cold case).
"""
import asyncio
import os
import shutil
import subprocess
import tarfile
import tempfile
import time
from backend.apps.outputs import view_builder_templates as vt
from backend.apps.outputs.runtime_proc import _find_free_port
from backend.apps.outputs.runtime import AppRuntime
HERE = os.path.dirname(os.path.abspath(__file__))
CSV = os.path.join(HERE, "appbuilder_breakdown.csv")
TMP = tempfile.mkdtemp(prefix="ab-measure-")
TMPL_FRONTEND = os.path.join(vt.WEBAPP_TEMPLATE_DIR, "frontend")
VITE_DEADLINE = 90
with open(CSV, "w", encoding="utf-8") as f:
f.write("phase,ms,note\n")
def lap(t):
return round((time.perf_counter() - t) * 1000)
def record(name, ms, note=""):
print(f"{ms:8d} ms {name}" + (f" ({note})" if note else ""), flush=True)
with open(CSV, "a", encoding="utf-8") as f:
f.write(f'"{name}",{ms},"{note}"\n')
f.flush()
def phase_seed():
ws = os.path.join(TMP, "ws-seed")
t = time.perf_counter()
vt.seed_webapp_template_workspace(ws, _find_free_port())
ms = lap(t)
present = os.path.exists(os.path.join(ws, "frontend", "node_modules"))
record("seed workspace + link node_modules (per app)", ms, "nm linked" if present else "NO nm")
def phase_npm_and_extract():
npm = vt._resolve_npm()
if not npm:
record("download: npm install (cold)", -1, "skipped: no npm")
return
work = os.path.join(TMP, "npm_cold")
os.makedirs(work, exist_ok=True)
shutil.copyfile(os.path.join(TMPL_FRONTEND, "package.json"), os.path.join(work, "package.json"))
lock = os.path.join(TMPL_FRONTEND, "package-lock.json")
cmd = [*npm, "install", "--prefer-offline", "--no-audit", "--no-fund", "--loglevel=error"]
if os.path.exists(lock):
shutil.copyfile(lock, os.path.join(work, "package-lock.json"))
cmd = [*npm, "ci", "--prefer-offline", "--no-audit", "--no-fund", "--loglevel=error"]
t = time.perf_counter()
try:
r = subprocess.run(cmd, cwd=work, capture_output=True, text=True, timeout=240)
record("download: npm install (cold, no archive)", lap(t), "ok" if r.returncode == 0 else f"rc={r.returncode}")
except subprocess.TimeoutExpired:
record("download: npm install (cold, no archive)", -1, "TIMEOUT 240s")
return
nm = os.path.join(work, "node_modules")
if not os.path.isdir(nm):
return
# Reuse that node_modules to time the archive build + extract (new path).
archive = os.path.join(TMP, "nm.tar.gz")
t = time.perf_counter()
with tarfile.open(archive, "w:gz") as tar:
tar.add(nm, arcname="node_modules")
record("build-time: tar node_modules -> archive", lap(t), f"{os.path.getsize(archive)//(1024*1024)}MB")
exd = os.path.join(TMP, "extract"); os.makedirs(exd, exist_ok=True)
t = time.perf_counter()
with tarfile.open(archive, "r:gz") as tar:
tar.extractall(exd)
record("download: archive extract (new build path)", lap(t))
async def _bind_once(label, vite_cache_dir):
ws = os.path.join(TMP, f"ws-{label}")
vt.seed_webapp_template_workspace(ws, _find_free_port())
if vite_cache_dir:
os.environ["OPENSWARM_VITE_CACHE_DIR"] = vite_cache_dir
else:
os.environ.pop("OPENSWARM_VITE_CACHE_DIR", None)
rt = AppRuntime(f"ws-{label}", ws)
t = time.perf_counter()
await rt.start()
deadline = time.perf_counter() + VITE_DEADLINE
while rt.frontend_url is None and time.perf_counter() < deadline:
await asyncio.sleep(0.1)
bound = rt.frontend_url is not None
ms = lap(t) if bound else -1
try:
await rt.stop()
except Exception:
pass
record(f"vite bind ({label})", ms, "bound" if bound else f"TIMEOUT {VITE_DEADLINE}s")
async def main():
print(f"temp: {TMP}", flush=True)
for fn in (phase_seed, phase_npm_and_extract):
try:
fn()
except Exception as e:
record(fn.__name__, -1, f"ERR {type(e).__name__}: {e}")
for label, cache in (("cold vite cache", os.path.join(TMP, "vite_cold")), ("warm shared cache", None)):
try:
await _bind_once(label, cache)
except Exception as e:
record(f"vite bind ({label})", -1, f"ERR {type(e).__name__}: {e}")
print("done", flush=True)
shutil.rmtree(TMP, ignore_errors=True)
if __name__ == "__main__":
asyncio.run(main())