[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
This commit is contained in:
Eric
2026-06-16 21:58:39 -07:00
parent ec99d24b8e
commit 24a767eec9
13 changed files with 815 additions and 0 deletions
+173
View File
@@ -0,0 +1,173 @@
# winv2: Windows startup + App Builder speed and bug fixes
Branch: `eric/winv2`. Goal: profile the real Windows experience first, find the
biggest bottleneck before changing anything, then fix the two reported bugs and
make startup + first-app download feel instant. All numbers below are measured
on the **real installed packaged app** (Squirrel install at
`AppData/Local/openswarm`, latest `app-1.2.82`), Windows 11, not dev mode.
Notion tracking (Todos DB):
- [Perf] Windows startup + download speed: backend cold-start is the bottleneck
- [App Builder] Windows preview broken: no bundled bash/npm + missing node_modules archive
- [Bug] Skills list empty until reboots + onboarding "Install a skill" step times out
- [Reliability] Distributed-systems hardening (design)
## How these numbers were measured
Source of truth: the packaged app's own perf markers in
`AppData/Roaming/openswarm/data/backend.log` (`[perf] app-launch`,
`[perf] first-paint`, `[perf] backend-http-ready`, written by `electron/main.js`).
These are wall-clock ms from process start, i.e. exactly what the user feels.
Raw extract: `baseline_startup.csv`. Re-run with `profile_startup.sh`.
Import cost measured with the bundled interpreter:
`python-env/python.exe -X importtime -c "import backend.main"`.
## Baseline (BEFORE any change)
### Startup, per launch (ms)
| metric | warm (typical) | cold (first run after each update) |
| --- | --- | --- |
| app-launch (electron ready) | 107-400 | 107-563 |
| first-paint (renderer) | 338-1205 | ~1200 |
| **backend-http-ready** | **8700-10500** | **54600 / 81000 / 86300 / 133000 / 138300** |
Electron shell paints in well under 1.5s every time. The Python backend is the
whole story: ~9-10s warm, and **54-138 seconds** on a cold/post-update launch.
First-agent-response figures in the log are dominated by user think-time and are
not treated as a startup metric.
### Why the backend is slow (evidence)
| factor | measurement | effect |
| --- | --- | --- |
| python-env file count | 13,554 files (4,510 .py/.pyd/.dll), 484 MB | Windows Defender real-time scan of every file on the first run after each update = the 1-2 minute cold spikes |
| app.asar size | 639 MB | cold disk read on first launch |
| backend.main import tree | ~2.2 s warm (`-X importtime`) | floor on warm boot, before interpreter init + lifespans |
| debugger project scan | runs at import (DEBUGLETON / build_structure) | extra warm boot time on the critical path |
| SubApp lifespans | entered sequentially in `config/Apps.py` before HTTP bind | serialized startup I/O |
## Bottleneck ranking (before changes)
1. **Python backend cold-start (dominant).** 9-10s warm, 54-138s cold. ~95% of
perceived startup. Cold case driven by Defender scanning 13.5k files + the
639 MB asar; warm case by import tree + debugger scan + serial lifespans.
2. **App Builder first-app on Windows is fully broken** (Bug #2): no bundled
bash, bundled node has no npm, and the Windows build ships no node_modules
archive. Confirmed against the installed binary. Until fixed, "download time"
for an app is effectively infinite (it never succeeds on a clean machine).
3. **Skills registry network race** (Bug #1): empty catalog until reboot, breaks
the onboarding "Install a skill" step (15s selector timeout).
## Plan (status tracked here + on Notion)
- [~] Bug #2 App Builder: **junction/copy link fallback DONE + tested**; archive in Windows build + direct vite spawn (no bash) TODO
- [~] Bug #1 Skills: **bundled snapshot + disk cache + retry-until-success DONE + tested** (catalog never empty offline, onboarding pdf selector resolves); frontend loading-vs-empty retry TODO
- [ ] Perf: trim Defender surface, lazy imports, non-blocking lifespans, move debugger scan off boot, App Builder warm pool
- [ ] Re-measure, before/after tables + graphs
## Progress log
- 2026-06-16 baseline measured (this doc), graphs generated, Notion todos opened.
- 2026-06-16 Bug #1 backend: `skill_registry.py` now seeds from bundled `skills_snapshot.json` + on-disk last-good cache and retries until first success. Proven non-empty fully offline (17 skills, search+stats green); `pdf` skill present so onboarding `skill-item-pdf` resolves. Regression test `backend/tests/test_skill_registry_seed.py` (3 cases green).
- 2026-06-16 Bug #2 link: `_link_node_modules` now falls back symlink -> junction (`mklink /J`, no admin) -> copy, so node_modules links even on a locked-down Windows box. Tested with forced symlink failure.
## Results (AFTER)
### The warm-startup bottleneck was found and fixed
Per-SubApp-lifespan profiling (`profile_boot.py`) showed the entire ~8s gap was
**one lifespan**:
| boot phase | before | after | note |
| --- | --- | --- | --- |
| import backend.main | 798 ms | 764 ms | unchanged (debugger scan is only ~80 ms) |
| **service lifespan** | **7412 ms** | **84 ms** | was `await ensure_9router()` blocking the HTTP bind |
| other 15 lifespans | 45 ms | 9 ms | all trivial |
| **import + lifespans floor** | **8256 ms** | **857 ms** | ~7.4 s removed (~90%) |
Fix: `service.py` now starts 9Router in the **background** instead of awaiting it
on the boot path. 9Router is only needed when the user sends an agent message,
and the dispatch path already calls `ensure_running()` (now lock-serialized in
`process.py` so the background start and a dispatch-time ensure can't
double-spawn). Net: warm backend-http-ready should drop from ~9-10 s to ~2-3 s,
comfortably under the 10 s goal. See `boot_breakdown.svg`.
### Still open (cold start)
The 54-138 s cold spikes are Windows Defender scanning the 13,554-file / 484 MB
python-env on the first run after each update, plus cold-reading the 639 MB
asar. That is a packaging change (fewer/larger files, trusted-location, or
zipped stdlib) and is higher-risk, tracked separately. The 9Router backgrounding
also helps cold (it no longer compounds the Defender wait).
### App Builder first-app "download" + create path (measured)
Per-phase, measured on this Windows box (`measure_appbuilder.py` + `measure_vite.py`),
isolated temp dirs, real warm caches. See `appbuilder_breakdown.svg`.
| phase | time | when it's paid |
| --- | --- | --- |
| seed workspace + link node_modules | 67 ms | every app (instant; junction/symlink to warm cache) |
| download: archive extract (new build path) | 14.2 s | once per machine/template version (Defender-bound: 215 MB nm) |
| download: npm install (cold fallback) | 42.7 s | once, only if no archive ships |
| vite bind: cold vite cache | 6.7 s | first app ever (esbuild pre-bundle) |
| vite bind: warm shared cache | 0.7 s | every subsequent app |
| build-time: tar nm -> archive | 6.8 s | on CI, never on the user's machine |
**User-facing scenarios (create app -> live preview):**
| scenario | total | notes |
| --- | --- | --- |
| first app, clean Windows, BEFORE fix | never works | `[WinError 2]` / "backend exited with code 1" (no bash/npm/archive) |
| first app, AFTER fix (tar archive) | ~21 s one-time | extract 14.2 + seed 0.07 + vite cold 6.7; and it actually works |
| **first app, AFTER fix + #9 item 2 (pre-extracted)** | **~7 s one-time (projected)** | **junction 0.07 + vite cold 6.7; the 14.2 s extract is gone** |
| first app, if we shipped npm instead | ~49 s | 42.7 + 6.7; the archive saves ~28 s and needs no npm |
| every subsequent app | ~0.8 s | seed 0.07 + vite warm 0.7 (near-instant) |
#9 item 2 (DONE): the Windows build now ships node_modules ALREADY EXTRACTED in
resources (digest-tagged); `_ensure_warm_cache` junctions a workspace straight at
it (`_bundled_extracted_modules`), so there is no tar-extract on first app -- the
14.2 s Defender-scanned write cost moves to install time, once. Verified by
`backend/tests/test_bundled_extracted_modules.py` (selection + Mac fallback) and
the build step `build-app-win.ps1` 4b now robocopies the tree into resources.
Takeaways: the archive (Bug #2 fix) turns a broken/∞ first-app into a working
~21s one-time, and ~0.8s for every app after. The remaining ~14s extract is the
SAME Defender-on-many-small-files cost as cold app-startup (Task #9) -- the one
lever that would shrink both.
### Net time decreased per step (measured)
| step | before | after | saved |
| --- | --- | --- | --- |
| backend boot: service lifespan | 7412 ms | 84 ms | -7328 ms (-99%) |
| backend boot: import + all lifespans floor | 8256 ms | 857 ms | -7399 ms (-90%) |
| backend-http-ready warm (end-to-end) | ~9-10 s | ~2-3 s (projected) | ~-7 s |
| App Builder dependency download | 42.7 s npm | 14.2 s archive | -28.5 s (-67%) |
| App Builder first app -> preview | broken/never | ~21 s working | inf -> 21 s |
| App Builder subsequent app -> preview | n/a | ~0.8 s | near-instant |
| skills catalog availability | empty until reboot(s) | instant (seeded) | bug eliminated |
## #9 packaging approach: shrink the Defender file surface (build-gated)
Defender real-time-scans every small file: python-env = 13,554 files; node_modules
= ~tens of thousands; app.asar = 639 MB. It rescans python-env on the first launch
after each update (54-138 s cold spikes) and scans node_modules as it is written
(the 14.2 s extract). Fix family: fewer/larger files, scan-once-at-install instead
of per-launch / per-first-app. Each item is independent, reversible, and must be
validated on a real packaged EXE (Task #10).
1. [DRAFTED, build-gated] Zip the Python stdlib -> python313.zip (medium risk). Draft: scripts/zip-python-stdlib.ps1 (dry-run by default; NOT wired into the release build yet). Measured on the real env: 910 stdlib .py/.pyc files (15.1 MB) collapse into one zip. CPython auto-adds <prefix>/python313.zip to sys.path, so no python._pth is needed; site-packages + DLLs (native .pyd) stay loose; a keep-list keeps data-file stdlib dirs (lib2to3, idlelib, tkinter, ...) loose. Impact: ~7% of total python-env file count, but it collapses the stdlib import-time file-opens (the cold-launch Defender scan storm) into a single scanned file; bigger combined with #3. Validation (Task #10): -Apply on a copy, then import backend.main, importtime parity, boot the packaged backend, measure cold backend-http-ready vs baseline. Wire into build-app-win.ps1 behind an off-by-default -ZipStdlib switch only after it passes.
2. [DONE] Ship webapp_template node_modules PRE-EXTRACTED in resources + junction to it (kills the 14.2 s extract -> ~0 s). build-app-win.ps1 step 4b robocopies the tree into resources; runtime _bundled_extracted_modules()/_ensure_warm_cache() prefer it; tests in test_bundled_extracted_modules.py. Mac still ships the .tar.gz (unchanged).
3. Precompile + ship only .pyc (drop .py) for app + pure-python deps. Halves remaining loose-file count; low risk; stacks with #1.
4. Inventory + trim app.asar (639 MB): source maps, dev-only deps, duplicate bundles. Single file (not a count issue) but shrinks cold-read I/O.
5. Opt-in Defender exclusion for install/data dirs, documented, never silent (needs admin/UAC; security-sensitive). Settings toggle only; do not auto-apply.
Recommended order: #2 (biggest UX win, lowest risk), then #1 (largest cold win, careful import testing), then #3/#4. Validation: re-run profile_startup.sh + a fresh-extract timing on the packaged EXE after each change, diff vs baseline_startup.csv.
### Bug fixes (this branch)
- Bug #1 skills: seed from bundled snapshot + disk cache + retry-until-success. Catalog never empty offline; 3 tests green; onboarding `skill-item-pdf` resolves.
- Bug #2 App Builder: (a) `_link_node_modules` symlink->junction->copy fallback (tested); (b) Windows-only direct `vite` spawn via bundled node so frontend-only apps need no bash (kills `[WinError 2]`); (c) `build-app-win.ps1` now pre-builds the node_modules archive natively. Verified end to end on Windows: build digest == runtime `_warm_cache_digest` (`37335fdd1f4d`); the archive (26 MB) extracts to a working node_modules containing `vite/bin/vite.js` and the Windows-native `@esbuild/win32-x64/esbuild.exe`.
+7
View File
@@ -0,0 +1,7 @@
phase,ms,note
"seed workspace + link node_modules (per app)",67,"nm linked, instant"
"download: archive extract (new build path, one-time)",14204,"215MB nm, defender-bound"
"download: npm install (cold fallback, one-time)",42684,"ok"
"vite bind: cold vite cache (first app)",6714,"bound"
"vite bind: warm shared cache (subsequent)",672,"bound"
"build-time: tar node_modules to archive (CI, not user)",6809,"26MB archive"
1 phase ms note
2 seed workspace + link node_modules (per app) 67 nm linked, instant
3 download: archive extract (new build path, one-time) 14204 215MB nm, defender-bound
4 download: npm install (cold fallback, one-time) 42684 ok
5 vite bind: cold vite cache (first app) 6714 bound
6 vite bind: warm shared cache (subsequent) 672 bound
7 build-time: tar node_modules to archive (CI, not user) 6809 26MB archive
+22
View File
@@ -0,0 +1,22 @@
<svg xmlns="http://www.w3.org/2000/svg" width="980" height="350" font-family="Segoe UI, sans-serif">
<text x="20" y="30" font-size="17" font-weight="600" fill="#1a1d27">App Builder "create app -> live preview" breakdown (ms)</text>
<text x="310" y="76" font-size="12" fill="#1a1d27" text-anchor="end">seed workspace + link node_modules (per app)</text>
<rect x="320" y="56" width="1.0" height="30" fill="#2e9e5b" rx="3"/>
<text x="329" y="76" font-size="12" fill="#1a1d27">67ms</text>
<text x="310" y="120" font-size="12" fill="#1a1d27" text-anchor="end">download: archive extract (new build path, one-time)</text>
<rect x="320" y="100" width="189.7" height="30" fill="#2e9e5b" rx="3"/>
<text x="518" y="120" font-size="12" fill="#1a1d27">14.20s</text>
<text x="310" y="164" font-size="12" fill="#1a1d27" text-anchor="end">download: npm install (cold fallback, one-time)</text>
<rect x="320" y="144" width="570.0" height="30" fill="#d64545" rx="3"/>
<text x="898" y="164" font-size="12" fill="#1a1d27">42.68s</text>
<text x="310" y="208" font-size="12" fill="#1a1d27" text-anchor="end">vite bind: cold vite cache (first app)</text>
<rect x="320" y="188" width="89.7" height="30" fill="#d64545" rx="3"/>
<text x="418" y="208" font-size="12" fill="#1a1d27">6.71s</text>
<text x="310" y="252" font-size="12" fill="#1a1d27" text-anchor="end">vite bind: warm shared cache (subsequent)</text>
<rect x="320" y="232" width="9.0" height="30" fill="#2e9e5b" rx="3"/>
<text x="337" y="252" font-size="12" fill="#1a1d27">672ms</text>
<text x="310" y="296" font-size="12" fill="#1a1d27" text-anchor="end">build-time: tar node_modules to archive (CI, not user)</text>
<rect x="320" y="276" width="90.9" height="30" fill="#2e9e5b" rx="3"/>
<text x="419" y="296" font-size="12" fill="#1a1d27">6.81s</text>
<text x="20" y="340" font-size="11" fill="#8892a4">green = warm/per-app cost; red = cold one-time download (npm with no archive)</text>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

+12
View File
@@ -0,0 +1,12 @@
<svg xmlns="http://www.w3.org/2000/svg" width="900" height="260" font-family="Segoe UI, sans-serif">
<text x="20" y="28" font-size="17" font-weight="600" fill="#1a1d27">where startup time goes (backend dwarfs the shell)</text>
<text x="20" y="77" font-size="13" fill="#1a1d27">typical warm launch</text>
<rect x="170" y="50" width="77.8" height="46" fill="#2e9e5b" rx="3"/>
<rect x="170" y="50" width="5.5" height="46" fill="#1a1d27" rx="3"/>
<text x="256" y="77" font-size="12" fill="#1a1d27">backend 9.6s (shell 0.68s)</text>
<text x="20" y="149" font-size="13" fill="#1a1d27">typical cold launch</text>
<rect x="170" y="122" width="700.0" height="46" fill="#d64545" rx="3"/>
<rect x="170" y="122" width="12.0" height="46" fill="#1a1d27" rx="3"/>
<text x="878" y="149" font-size="12" fill="#1a1d27">backend 86.3s (shell 1.48s)</text>
<text x="20" y="248" font-size="11" fill="#8892a4">dark = electron shell (app-launch + first-paint); colored = python backend</text>
</svg>

After

Width:  |  Height:  |  Size: 977 B

+15
View File
@@ -0,0 +1,15 @@
launch_ts,version,app_launch_ms,first_paint_ms,backend_http_ready_ms,class
2026-06-02T09:18:13Z,1.1.72,380,1097,54606,cold
2026-06-08T22:16:53Z,1.2.73,143,636,10463,warm
2026-06-08T23:03:47Z,1.2.73,317,625,10084,warm
2026-06-08T23:58:30Z,1.2.73,198,515,81041,cold
2026-06-09T03:13:05Z,1.2.73,147,559,10418,warm
2026-06-09T11:04:12Z,1.2.75,129,609,10041,warm
2026-06-09T11:53:18Z,1.2.75,108,338,8761,warm
2026-06-10T07:44:10Z,1.2.75,388,1100,86310,cold
2026-06-10T07:46:53Z,1.2.76,112,389,9342,warm
2026-06-10T23:32:57Z,1.2.76,563,1205,133070,cold
2026-06-10T23:35:13Z,1.2.77,114,391,9349,warm
2026-06-10T23:35:41Z,1.2.77,122,404,9303,warm
2026-06-14T00:07:35Z,1.2.77,159,1213,138335,cold
2026-06-14T00:09:58Z,1.2.82,107,702,9590,warm
1 launch_ts version app_launch_ms first_paint_ms backend_http_ready_ms class
2 2026-06-02T09:18:13Z 1.1.72 380 1097 54606 cold
3 2026-06-08T22:16:53Z 1.2.73 143 636 10463 warm
4 2026-06-08T23:03:47Z 1.2.73 317 625 10084 warm
5 2026-06-08T23:58:30Z 1.2.73 198 515 81041 cold
6 2026-06-09T03:13:05Z 1.2.73 147 559 10418 warm
7 2026-06-09T11:04:12Z 1.2.75 129 609 10041 warm
8 2026-06-09T11:53:18Z 1.2.75 108 338 8761 warm
9 2026-06-10T07:44:10Z 1.2.75 388 1100 86310 cold
10 2026-06-10T07:46:53Z 1.2.76 112 389 9342 warm
11 2026-06-10T23:32:57Z 1.2.76 563 1205 133070 cold
12 2026-06-10T23:35:13Z 1.2.77 114 391 9349 warm
13 2026-06-10T23:35:41Z 1.2.77 122 404 9303 warm
14 2026-06-14T00:07:35Z 1.2.77 159 1213 138335 cold
15 2026-06-14T00:09:58Z 1.2.82 107 702 9590 warm
+57
View File
@@ -0,0 +1,57 @@
<svg xmlns="http://www.w3.org/2000/svg" width="900" height="420" font-family="Segoe UI, sans-serif">
<text x="60" y="28" font-size="17" font-weight="600" fill="#1a1d27">backend-http-ready per launch (ms) - lower is better</text>
<line x1="60" y1="330" x2="880" y2="330" stroke="#e2e6ef"/>
<text x="52" y="334" font-size="11" fill="#8892a4" text-anchor="end">0s</text>
<line x1="60" y1="260" x2="880" y2="260" stroke="#e2e6ef"/>
<text x="52" y="264" font-size="11" fill="#8892a4" text-anchor="end">35s</text>
<line x1="60" y1="190" x2="880" y2="190" stroke="#e2e6ef"/>
<text x="52" y="194" font-size="11" fill="#8892a4" text-anchor="end">69s</text>
<line x1="60" y1="120" x2="880" y2="120" stroke="#e2e6ef"/>
<text x="52" y="124" font-size="11" fill="#8892a4" text-anchor="end">104s</text>
<line x1="60" y1="50" x2="880" y2="50" stroke="#e2e6ef"/>
<text x="52" y="54" font-size="11" fill="#8892a4" text-anchor="end">138s</text>
<rect x="68.8" y="219.5" width="41.0" height="110.5" fill="#d64545" rx="2"/>
<text x="89.3" y="214.5" font-size="10" fill="#1a1d27" text-anchor="middle">55s</text>
<text x="89.3" y="346" font-size="9" fill="#8892a4" text-anchor="end" transform="rotate(-40 89.3 346)">1.1.72</text>
<rect x="127.4" y="308.8" width="41.0" height="21.2" fill="#2e9e5b" rx="2"/>
<text x="147.9" y="303.8" font-size="10" fill="#1a1d27" text-anchor="middle">10s</text>
<text x="147.9" y="346" font-size="9" fill="#8892a4" text-anchor="end" transform="rotate(-40 147.9 346)">1.2.73</text>
<rect x="185.9" y="309.6" width="41.0" height="20.4" fill="#2e9e5b" rx="2"/>
<text x="206.4" y="304.6" font-size="10" fill="#1a1d27" text-anchor="middle">10s</text>
<text x="206.4" y="346" font-size="9" fill="#8892a4" text-anchor="end" transform="rotate(-40 206.4 346)">1.2.73</text>
<rect x="244.5" y="166.0" width="41.0" height="164.0" fill="#d64545" rx="2"/>
<text x="265.0" y="161.0" font-size="10" fill="#1a1d27" text-anchor="middle">81s</text>
<text x="265.0" y="346" font-size="9" fill="#8892a4" text-anchor="end" transform="rotate(-40 265.0 346)">1.2.73</text>
<rect x="303.1" y="308.9" width="41.0" height="21.1" fill="#2e9e5b" rx="2"/>
<text x="323.6" y="303.9" font-size="10" fill="#1a1d27" text-anchor="middle">10s</text>
<text x="323.6" y="346" font-size="9" fill="#8892a4" text-anchor="end" transform="rotate(-40 323.6 346)">1.2.73</text>
<rect x="361.6" y="309.7" width="41.0" height="20.3" fill="#2e9e5b" rx="2"/>
<text x="382.1" y="304.7" font-size="10" fill="#1a1d27" text-anchor="middle">10s</text>
<text x="382.1" y="346" font-size="9" fill="#8892a4" text-anchor="end" transform="rotate(-40 382.1 346)">1.2.75</text>
<rect x="420.2" y="312.3" width="41.0" height="17.7" fill="#2e9e5b" rx="2"/>
<text x="440.7" y="307.3" font-size="10" fill="#1a1d27" text-anchor="middle">9s</text>
<text x="440.7" y="346" font-size="9" fill="#8892a4" text-anchor="end" transform="rotate(-40 440.7 346)">1.2.75</text>
<rect x="478.8" y="155.3" width="41.0" height="174.7" fill="#d64545" rx="2"/>
<text x="499.3" y="150.3" font-size="10" fill="#1a1d27" text-anchor="middle">86s</text>
<text x="499.3" y="346" font-size="9" fill="#8892a4" text-anchor="end" transform="rotate(-40 499.3 346)">1.2.75</text>
<rect x="537.4" y="311.1" width="41.0" height="18.9" fill="#2e9e5b" rx="2"/>
<text x="557.9" y="306.1" font-size="10" fill="#1a1d27" text-anchor="middle">9s</text>
<text x="557.9" y="346" font-size="9" fill="#8892a4" text-anchor="end" transform="rotate(-40 557.9 346)">1.2.76</text>
<rect x="595.9" y="60.7" width="41.0" height="269.3" fill="#d64545" rx="2"/>
<text x="616.4" y="55.7" font-size="10" fill="#1a1d27" text-anchor="middle">133s</text>
<text x="616.4" y="346" font-size="9" fill="#8892a4" text-anchor="end" transform="rotate(-40 616.4 346)">1.2.76</text>
<rect x="654.5" y="311.1" width="41.0" height="18.9" fill="#2e9e5b" rx="2"/>
<text x="675.0" y="306.1" font-size="10" fill="#1a1d27" text-anchor="middle">9s</text>
<text x="675.0" y="346" font-size="9" fill="#8892a4" text-anchor="end" transform="rotate(-40 675.0 346)">1.2.77</text>
<rect x="713.1" y="311.2" width="41.0" height="18.8" fill="#2e9e5b" rx="2"/>
<text x="733.6" y="306.2" font-size="10" fill="#1a1d27" text-anchor="middle">9s</text>
<text x="733.6" y="346" font-size="9" fill="#8892a4" text-anchor="end" transform="rotate(-40 733.6 346)">1.2.77</text>
<rect x="771.6" y="50.0" width="41.0" height="280.0" fill="#d64545" rx="2"/>
<text x="792.1" y="45.0" font-size="10" fill="#1a1d27" text-anchor="middle">138s</text>
<text x="792.1" y="346" font-size="9" fill="#8892a4" text-anchor="end" transform="rotate(-40 792.1 346)">1.2.77</text>
<rect x="830.2" y="310.6" width="41.0" height="19.4" fill="#2e9e5b" rx="2"/>
<text x="850.7" y="305.6" font-size="10" fill="#1a1d27" text-anchor="middle">10s</text>
<text x="850.7" y="346" font-size="9" fill="#8892a4" text-anchor="end" transform="rotate(-40 850.7 346)">1.2.82</text>
<rect x="700" y="50" width="12" height="12" fill="#2e9e5b"/><text x="716" y="61" font-size="12" fill="#1a1d27">warm</text>
<rect x="770" y="50" width="12" height="12" fill="#d64545"/><text x="786" y="61" font-size="12" fill="#1a1d27">cold (post-update)</text>
</svg>

After

Width:  |  Height:  |  Size: 5.0 KiB

+5
View File
@@ -0,0 +1,5 @@
phase,before_ms,after_ms
import backend.main,798,764
service lifespan (9router start),7412,84
other 15 lifespans,45,9
import + lifespans floor,8256,857
1 phase before_ms after_ms
2 import backend.main 798 764
3 service lifespan (9router start) 7412 84
4 other 15 lifespans 45 9
5 import + lifespans floor 8256 857
+31
View File
@@ -0,0 +1,31 @@
<svg xmlns="http://www.w3.org/2000/svg" width="900" height="360" font-family="Segoe UI, sans-serif">
<text x="60" y="28" font-size="17" font-weight="600" fill="#1a1d27">warm boot breakdown: before vs after (ms) - the service lifespan was the bottleneck</text>
<line x1="60" y1="240" x2="870" y2="240" stroke="#e2e6ef"/>
<text x="52" y="244" font-size="11" fill="#8892a4" text-anchor="end">0.0s</text>
<line x1="60" y1="145" x2="870" y2="145" stroke="#e2e6ef"/>
<text x="52" y="149" font-size="11" fill="#8892a4" text-anchor="end">4.1s</text>
<line x1="60" y1="50" x2="870" y2="50" stroke="#e2e6ef"/>
<text x="52" y="54" font-size="11" fill="#8892a4" text-anchor="end">8.3s</text>
<rect x="89.0" y="221.6" width="68.9" height="18.4" fill="#d64545" rx="2"/>
<text x="123.4" y="217.6" font-size="10" fill="#1a1d27" text-anchor="middle">0.8s</text>
<rect x="157.8" y="222.4" width="68.9" height="17.6" fill="#2e9e5b" rx="2"/>
<text x="192.2" y="218.4" font-size="10" fill="#1a1d27" text-anchor="middle">0.8s</text>
<text x="161.2" y="258" font-size="10" fill="#8892a4" text-anchor="end" transform="rotate(-25 161.2 258)">import backend.main</text>
<rect x="291.5" y="69.4" width="68.9" height="170.6" fill="#d64545" rx="2"/>
<text x="325.9" y="65.4" font-size="10" fill="#1a1d27" text-anchor="middle">7.4s</text>
<rect x="360.3" y="238.1" width="68.9" height="1.9" fill="#2e9e5b" rx="2"/>
<text x="394.7" y="234.1" font-size="10" fill="#1a1d27" text-anchor="middle">0.1s</text>
<text x="363.8" y="258" font-size="10" fill="#8892a4" text-anchor="end" transform="rotate(-25 363.8 258)">service lifespan (9router start)</text>
<rect x="494.0" y="239.0" width="68.9" height="1.0" fill="#d64545" rx="2"/>
<text x="528.4" y="235.0" font-size="10" fill="#1a1d27" text-anchor="middle">0.0s</text>
<rect x="562.8" y="239.8" width="68.9" height="0.2" fill="#2e9e5b" rx="2"/>
<text x="597.2" y="235.8" font-size="10" fill="#1a1d27" text-anchor="middle">0.0s</text>
<text x="566.2" y="258" font-size="10" fill="#8892a4" text-anchor="end" transform="rotate(-25 566.2 258)">other 15 lifespans</text>
<rect x="696.5" y="50.0" width="68.9" height="190.0" fill="#d64545" rx="2"/>
<text x="730.9" y="46.0" font-size="10" fill="#1a1d27" text-anchor="middle">8.3s</text>
<rect x="765.3" y="220.3" width="68.9" height="19.7" fill="#2e9e5b" rx="2"/>
<text x="799.7" y="216.3" font-size="10" fill="#1a1d27" text-anchor="middle">0.9s</text>
<text x="768.8" y="258" font-size="10" fill="#8892a4" text-anchor="end" transform="rotate(-25 768.8 258)">import + lifespans floor</text>
<rect x="700" y="50" width="12" height="12" fill="#d64545"/><text x="716" y="61" font-size="12" fill="#1a1d27">before</text>
<rect x="780" y="50" width="12" height="12" fill="#2e9e5b"/><text x="796" y="61" font-size="12" fill="#1a1d27">after</text>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

+190
View File
@@ -0,0 +1,190 @@
"""Dependency-free SVG charts for the winv2 perf baseline.
No matplotlib/pandas (not in the bundled env). Reads baseline_startup.csv and
writes two self-contained SVGs that render in a browser, GitHub, or Notion:
baseline_startup.svg - backend-http-ready per launch (warm vs cold)
baseline_phases.svg - where the time goes (app-launch / first-paint / backend)
Run: python make_graphs.py
"""
import csv
import os
HERE = os.path.dirname(os.path.abspath(__file__))
CSV = os.path.join(HERE, "baseline_startup.csv")
WARM = "#2e9e5b"
COLD = "#d64545"
INK = "#1a1d27"
MUTE = "#8892a4"
GRID = "#e2e6ef"
def rows():
with open(CSV, newline="", encoding="utf-8") as f:
return list(csv.DictReader(f))
def bars_chart(data):
w, h = 900, 420
pad_l, pad_b, pad_t, pad_r = 60, 90, 50, 20
plot_w = w - pad_l - pad_r
plot_h = h - pad_t - pad_b
vals = [int(r["backend_http_ready_ms"]) for r in data]
vmax = max(vals)
n = len(data)
bw = plot_w / n * 0.7
gap = plot_w / n
out = [f'<svg xmlns="http://www.w3.org/2000/svg" width="{w}" height="{h}" font-family="Segoe UI, sans-serif">']
out.append(f'<text x="{pad_l}" y="28" font-size="17" font-weight="600" fill="{INK}">'
'backend-http-ready per launch (ms) - lower is better</text>')
# y gridlines
for frac in (0, 0.25, 0.5, 0.75, 1.0):
yv = vmax * frac
y = pad_t + plot_h - plot_h * frac
out.append(f'<line x1="{pad_l}" y1="{y:.0f}" x2="{w-pad_r}" y2="{y:.0f}" stroke="{GRID}"/>')
out.append(f'<text x="{pad_l-8}" y="{y+4:.0f}" font-size="11" fill="{MUTE}" text-anchor="end">{yv/1000:.0f}s</text>')
for i, r in enumerate(data):
v = int(r["backend_http_ready_ms"])
bh = plot_h * v / vmax
x = pad_l + i * gap + (gap - bw) / 2
y = pad_t + plot_h - bh
color = COLD if r["class"] == "cold" else WARM
out.append(f'<rect x="{x:.1f}" y="{y:.1f}" width="{bw:.1f}" height="{bh:.1f}" fill="{color}" rx="2"/>')
out.append(f'<text x="{x+bw/2:.1f}" y="{y-5:.1f}" font-size="10" fill="{INK}" text-anchor="middle">{v/1000:.0f}s</text>')
out.append(f'<text x="{x+bw/2:.1f}" y="{h-pad_b+16:.0f}" font-size="9" fill="{MUTE}" '
f'text-anchor="end" transform="rotate(-40 {x+bw/2:.1f} {h-pad_b+16:.0f})">{r["version"]}</text>')
out.append(f'<rect x="{w-200}" y="{pad_t}" width="12" height="12" fill="{WARM}"/>'
f'<text x="{w-184}" y="{pad_t+11}" font-size="12" fill="{INK}">warm</text>')
out.append(f'<rect x="{w-130}" y="{pad_t}" width="12" height="12" fill="{COLD}"/>'
f'<text x="{w-114}" y="{pad_t+11}" font-size="12" fill="{INK}">cold (post-update)</text>')
out.append('</svg>')
return "\n".join(out)
def phases_chart(data):
warm = [r for r in data if r["class"] == "warm"]
cold = [r for r in data if r["class"] == "cold"]
def med(rows_, key):
xs = sorted(int(r[key]) for r in rows_)
return xs[len(xs) // 2] if xs else 0
cases = [
("typical warm launch", med(warm, "app_launch_ms"), med(warm, "first_paint_ms"), med(warm, "backend_http_ready_ms")),
("typical cold launch", med(cold, "app_launch_ms"), med(cold, "first_paint_ms"), med(cold, "backend_http_ready_ms")),
]
w, h = 900, 260
pad_l, pad_r, pad_t = 170, 30, 50
plot_w = w - pad_l - pad_r
vmax = max(c[3] for c in cases)
out = [f'<svg xmlns="http://www.w3.org/2000/svg" width="{w}" height="{h}" font-family="Segoe UI, sans-serif">']
out.append(f'<text x="20" y="28" font-size="17" font-weight="600" fill="{INK}">'
'where startup time goes (backend dwarfs the shell)</text>')
row_h = 46
for i, (label, al, fp, br) in enumerate(cases):
y = pad_t + i * (row_h + 26)
out.append(f'<text x="20" y="{y+row_h/2+4:.0f}" font-size="13" fill="{INK}">{label}</text>')
# backend is the full bar; app-launch+first-paint are the tiny left slice
bw_backend = plot_w * br / vmax
out.append(f'<rect x="{pad_l}" y="{y}" width="{bw_backend:.1f}" height="{row_h}" fill="{COLD if i==1 else WARM}" rx="3"/>')
shell = al + fp
bw_shell = plot_w * shell / vmax
out.append(f'<rect x="{pad_l}" y="{y}" width="{max(bw_shell,2):.1f}" height="{row_h}" fill="{INK}" rx="3"/>')
out.append(f'<text x="{pad_l+bw_backend+8:.0f}" y="{y+row_h/2+4:.0f}" font-size="12" fill="{INK}">'
f'backend {br/1000:.1f}s (shell {shell/1000:.2f}s)</text>')
out.append(f'<text x="20" y="{h-12}" font-size="11" fill="{MUTE}">'
'dark = electron shell (app-launch + first-paint); colored = python backend</text>')
out.append('</svg>')
return "\n".join(out)
def boot_chart():
"""Before/after grouped bars for the boot-phase breakdown (profile_boot.py)."""
path = os.path.join(HERE, "boot_breakdown.csv")
with open(path, newline="", encoding="utf-8") as f:
data = list(csv.DictReader(f))
w, h = 900, 360
pad_l, pad_r, pad_t, pad_b = 60, 30, 50, 120
plot_w = w - pad_l - pad_r
plot_h = h - pad_t - pad_b
vmax = max(max(int(r["before_ms"]), int(r["after_ms"])) for r in data)
n = len(data)
group = plot_w / n
bw = group * 0.34
out = [f'<svg xmlns="http://www.w3.org/2000/svg" width="{w}" height="{h}" font-family="Segoe UI, sans-serif">']
out.append(f'<text x="{pad_l}" y="28" font-size="17" font-weight="600" fill="{INK}">'
'warm boot breakdown: before vs after (ms) - the service lifespan was the bottleneck</text>')
for frac in (0, 0.5, 1.0):
y = pad_t + plot_h - plot_h * frac
out.append(f'<line x1="{pad_l}" y1="{y:.0f}" x2="{w-pad_r}" y2="{y:.0f}" stroke="{GRID}"/>')
out.append(f'<text x="{pad_l-8}" y="{y+4:.0f}" font-size="11" fill="{MUTE}" text-anchor="end">{vmax*frac/1000:.1f}s</text>')
for i, r in enumerate(data):
bx = pad_l + i * group + group / 2
for j, (key, color, lab) in enumerate((("before_ms", COLD, "before"), ("after_ms", WARM, "after"))):
v = int(r[key])
bh = plot_h * v / vmax
x = bx + (j - 1) * bw - bw * 0.05
y = pad_t + plot_h - bh
out.append(f'<rect x="{x:.1f}" y="{y:.1f}" width="{bw:.1f}" height="{bh:.1f}" fill="{color}" rx="2"/>')
out.append(f'<text x="{x+bw/2:.1f}" y="{y-4:.1f}" font-size="10" fill="{INK}" text-anchor="middle">{v/1000:.1f}s</text>')
out.append(f'<text x="{bx:.1f}" y="{h-pad_b+18:.0f}" font-size="10" fill="{MUTE}" text-anchor="end" '
f'transform="rotate(-25 {bx:.1f} {h-pad_b+18:.0f})">{r["phase"]}</text>')
out.append(f'<rect x="{w-200}" y="{pad_t}" width="12" height="12" fill="{COLD}"/><text x="{w-184}" y="{pad_t+11}" font-size="12" fill="{INK}">before</text>')
out.append(f'<rect x="{w-120}" y="{pad_t}" width="12" height="12" fill="{WARM}"/><text x="{w-104}" y="{pad_t+11}" font-size="12" fill="{INK}">after</text>')
out.append('</svg>')
return "\n".join(out)
def appbuilder_chart():
"""Horizontal bars for the App Builder create-path breakdown. Returns None
if the measurement CSV hasn't been generated yet."""
path = os.path.join(HERE, "appbuilder_breakdown.csv")
if not os.path.exists(path):
return None
with open(path, newline="", encoding="utf-8") as f:
raw = list(csv.DictReader(f))
# Keep only real timing phases (drop the boolean/skipped/-1 rows).
data = [r for r in raw if r["ms"].lstrip("-").isdigit() and int(r["ms"]) >= 0
and not r["phase"].strip().startswith("->")]
if not data:
return None
w = 980
row_h, gap, pad_t, pad_l, pad_r = 30, 14, 56, 320, 90
h = pad_t + len(data) * (row_h + gap) + 30
vmax = max(int(r["ms"]) for r in data) or 1
plot_w = w - pad_l - pad_r
out = [f'<svg xmlns="http://www.w3.org/2000/svg" width="{w}" height="{h}" font-family="Segoe UI, sans-serif">']
out.append(f'<text x="20" y="30" font-size="17" font-weight="600" fill="{INK}">'
'App Builder "create app -> live preview" breakdown (ms)</text>')
for i, r in enumerate(data):
v = int(r["ms"])
y = pad_t + i * (row_h + gap)
bw = max(plot_w * v / vmax, 1)
# download/npm = cold cost (red-ish), everything else = warm/per-app (green)
cold = ("npm" in r["phase"]) or ("cold" in r["phase"])
color = COLD if cold else WARM
out.append(f'<text x="{pad_l-10}" y="{y+row_h*0.68:.0f}" font-size="12" fill="{INK}" text-anchor="end">{r["phase"]}</text>')
out.append(f'<rect x="{pad_l}" y="{y}" width="{bw:.1f}" height="{row_h}" fill="{color}" rx="3"/>')
label = f'{v/1000:.2f}s' if v >= 1000 else f'{v}ms'
out.append(f'<text x="{pad_l+bw+8:.0f}" y="{y+row_h*0.68:.0f}" font-size="12" fill="{INK}">{label}</text>')
out.append(f'<text x="20" y="{h-10}" font-size="11" fill="{MUTE}">'
'green = warm/per-app cost; red = cold one-time download (npm with no archive)</text>')
out.append('</svg>')
return "\n".join(out)
def main():
data = rows()
open(os.path.join(HERE, "baseline_startup.svg"), "w", encoding="utf-8").write(bars_chart(data))
open(os.path.join(HERE, "baseline_phases.svg"), "w", encoding="utf-8").write(phases_chart(data))
open(os.path.join(HERE, "boot_breakdown.svg"), "w", encoding="utf-8").write(boot_chart())
wrote = "baseline_startup.svg + baseline_phases.svg + boot_breakdown.svg"
ab = appbuilder_chart()
if ab:
open(os.path.join(HERE, "appbuilder_breakdown.svg"), "w", encoding="utf-8").write(ab)
wrote += " + appbuilder_breakdown.svg"
print("wrote " + wrote)
if __name__ == "__main__":
main()
+135
View File
@@ -0,0 +1,135 @@
"""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())
+70
View File
@@ -0,0 +1,70 @@
"""Vite-bind-only measurement (winv2 Task #3, part 2).
Split out from measure_appbuilder.py because Python's tarfile gzip of a full
node_modules is pathologically slow and was eating the time budget before the
vite phases ran. This does ONLY the two vite binds (cold vite cache = first app
ever; warm shared cache = subsequent apps) and APPENDS to appbuilder_breakdown.csv.
No tar, no npm. Run unbuffered.
"""
import asyncio
import os
import shutil
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-vite-")
VITE_DEADLINE = 100
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()
async def bind_once(label, vite_cache_dir):
ws = os.path.join(TMP, f"ws-{label.replace(' ', '_')}")
vt.seed_webapp_template_workspace(ws, _find_free_port())
if not os.path.exists(os.path.join(ws, "frontend", "node_modules")):
record(f"vite bind ({label})", -1, "no node_modules linked")
return
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 = round((time.perf_counter() - t) * 1000) 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 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())
+67
View File
@@ -0,0 +1,67 @@
"""Per-phase + per-SubApp-lifespan boot profiler (winv2).
Warm import is ~1.3s but backend-http-ready is ~9-10s, so the gap is the
lifespan startup (SubApp lifespans are entered sequentially in config/Apps.py
before uvicorn serves). This times each one to find what blocks the HTTP bind.
Run with the bundled interpreter from the resources dir, e.g.:
python-env/python.exe docs/perf/winv2/profile_boot.py
It spawns the same subprocesses a real boot does (9router etc.); the
AsyncExitStack unwinds at the end. Kill any straggler node/9router after.
"""
import asyncio
import os
import time
os.environ.setdefault("OPENSWARM_AUTH_TOKEN", "x")
_t0 = time.perf_counter()
import backend.main # noqa: F401 (builds main_app; full import tree)
_import_ms = (time.perf_counter() - _t0) * 1000
from contextlib import AsyncExitStack # noqa: E402
from backend.apps.health.health import health # noqa: E402
from backend.apps.agents.agents import agents # noqa: E402
from backend.apps.skills.skills import skills # noqa: E402
from backend.apps.tools_lib.tools_lib import tools_lib # noqa: E402
from backend.apps.modes.modes import modes # noqa: E402
from backend.apps.settings.settings import settings # noqa: E402
from backend.apps.mcp_registry.mcp_registry import mcp_registry # noqa: E402
from backend.apps.skill_registry.skill_registry import skill_registry # noqa: E402
from backend.apps.outputs.outputs import outputs # noqa: E402
from backend.apps.dashboards.dashboards import dashboards # noqa: E402
from backend.apps.swarm.swarm import swarm # noqa: E402
from backend.apps.service.service import service # noqa: E402
from backend.apps.subscription.router import subscription # noqa: E402
from backend.apps.auth.router import auth # noqa: E402
from backend.apps.web.web import web # noqa: E402
from backend.apps.agents.proxy.anthropic_proxy import anthropic_proxy # noqa: E402
SUBS = [health, agents, skills, tools_lib, modes, settings, mcp_registry,
skill_registry, outputs, dashboards, swarm, service, subscription,
auth, web, anthropic_proxy]
async def main():
print(f"{_import_ms:8.0f} ms import backend.main (full tree)")
print("-" * 48)
total = 0.0
async with AsyncExitStack() as stack:
for s in SUBS:
t = time.perf_counter()
try:
await asyncio.wait_for(stack.enter_async_context(s.lifespan()), timeout=60)
except Exception as e:
print(f" ERR lifespan {s.name}: {type(e).__name__}")
continue
dt = (time.perf_counter() - t) * 1000
total += dt
print(f"{dt:8.0f} ms lifespan {s.name}")
print("-" * 48)
print(f"{total:8.0f} ms all lifespans")
print(f"{_import_ms + total:8.0f} ms import + lifespans (approx backend-ready floor)")
if __name__ == "__main__":
asyncio.run(main())
+31
View File
@@ -0,0 +1,31 @@
#!/bin/bash
# Re-extract real packaged-app startup timings from the installed app's backend
# log. Prints one row per launch: timestamp, version, app-launch ms,
# first-paint ms, backend-http-ready ms. Pipe to a CSV for the metrics table.
#
# Usage: bash profile_startup.sh [path-to-backend.log]
# Default log: AppData/Roaming/openswarm/data/backend.log
LOG="${1:-$HOME/AppData/Roaming/openswarm/data/backend.log}"
if [[ ! -f "$LOG" ]]; then
echo "no backend.log at $LOG" >&2
exit 1
fi
echo "launch_ts,version,app_launch_ms,first_paint_ms,backend_http_ready_ms,class"
awk '
/===== launch/ {
if (ts != "") emit()
ts=$3; ver=""
for (i=1;i<=NF;i++) if ($i ~ /^\(app$/) { ver=$(i+1); gsub(/,/,"",ver) }
al=""; fp=""; br=""
}
/\[perf\] app-launch t=/ { sub(/.*t=/,""); al=$0 }
/\[perf\] first-paint t=/ { sub(/.*t=/,""); fp=$0 }
/\[perf\] backend-http-ready t=/ { sub(/.*t=/,""); br=$0 }
END { if (ts != "") emit() }
function emit() {
cls = (br+0 > 20000) ? "cold" : "warm"
printf "%s,%s,%s,%s,%s,%s\n", ts, ver, al, fp, br, cls
}
' "$LOG"