Files
5chan/.codex/skills/profile-browsing/SKILL.md
T
Tommaso Casaburi 1a33f7dc88 chore(agents): add a machine-wide Playwright browser resource budget
Playwright disables normal background throttling, so a hidden 5chan page keeps
doing P2P and rendering work after a check finishes. Agents verifying in
parallel across worktrees stacked whole browser engines on one machine.

Add scripts/pw-session.sh, a wrapper that permits one active Playwright browser
at a time and records who holds it:

- The lock is machine-wide, not per-repository, because the contended resource
  is RAM and CPU. Every worktree and checkout shares one slot.
- Acquisition is an atomic mkdir. Stale locks clear themselves: `open` reclaims
  any slot whose recorded browser is no longer `status: open` in
  `playwright-cli list --all`, so an interrupted workflow cannot strand the
  budget. When that list cannot be read the lock is left alone, so a broken CLI
  never silently disables the budget.
- `open` exits 75 when the slot is busy; `--wait[=SECONDS]` blocks instead.
- `close` always stops the browser, even when the lock was already lost, and
  never releases a slot held by a different session.
- `status` reports the holder and whether its browser is still alive.

Agent policy now runs browser engines and profiler batches sequentially, uses
Chrome/Blink during iteration and the full engine matrix only for final
verification, and never uses `close-all` or `kill-all` while other agents may
own sessions.

Covered by scripts/pw-session.test.js.
2026-08-01 19:21:01 +02:00

165 lines
8.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
name: profile-browsing
description: Profile app performance while browsing, collecting Web Vitals and React rerender data via react-scan. Orchestrates sequential profiler subagents via playwright-cli to capture navigation timing, long tasks, layout shifts, LCP, React commit counts, render bursts, and per-component render data without saturating the machine. Use when profiling browsing performance, finding bottlenecks, diagnosing excessive rerenders, or auditing page performance.
---
# Profile Browsing Performance
Two-layer profiling: browser-level symptoms (Web Vitals, long tasks, scroll jank) and React-level diagnosis (commit counts, render bursts, per-component render data from react-scan). Each profiler subagent runs in its own browser session and context window, with only one profiler active at a time.
## Prerequisites
- Dev server running at https://5chan.localhost (`yarn start` via Portless)
- `playwright-cli` installed (`npm install -g @playwright/cli@latest`)
**IMPORTANT:** The orchestrator (you) is responsible for ensuring exactly ONE dev server is running. Profiler subagents must NEVER start a dev server themselves.
### react-scan (already configured)
`src/lib/react-scan.ts` runs react-scan in dev mode. It:
- Highlights rerendering components visually (toolbar + overlay)
- Accumulates per-component render counts and times via react-scan's `onRender` option
- Exposes `window.__getReactScanReport()` and `window.__resetReactScanReport()` for programmatic collection
`__getReactScanReport()` returns a plain object: `{ ComponentName: { count, time } }`.
**Do not use react-scan's own `getReport()`.** It reads `Store.legacyReportData`, which react-scan 0.5.3 never writes to, so it always returns an empty `Map`. The live `Store.reportData` is no better: it is only populated while the toolbar is visible *and* a component is manually focused in the inspector, neither of which holds under automation. The app's `onRender` collector exists precisely because of this. Also note a `Map` cannot be serialized — `JSON.stringify(new Map())` is `"{}"` regardless of contents — which is why the collector returns a plain object.
The profiler's `addInitScript` sets `window.__PROFILING__ = true` before the app loads, which tells react-scan to disable its toolbar and sounds during automated runs.
No additional setup needed — react-scan is already a devDependency and imported in the entry file.
## Step 0: Ensure Dev Server is Running
Before running any profiler subagents, verify exactly one dev server is available:
```bash
# Check if the dev server is reachable
curl -sf https://5chan.localhost -o /dev/null && echo "OK" || echo "NOT RUNNING"
```
- If **OK**: proceed to Step 1.
- If **NOT RUNNING**: start one instance with `yarn start` (backgrounded), then poll until it responds. Do NOT start more than one.
- If a dev server is already running on a different port (check `ps aux | grep vite`), reuse it — do not start another.
## Step 1: Define Route Batches
Split routes into batches of 24 for sequential profiling. Give every batch a short task-specific session name so unrelated profiling runs cannot collide.
**Default batches** (adjust boards as needed):
| Batch | Session | Routes | Focus |
|-------|---------|--------|-------|
| 1 | `prof-1` | `/all`, `/all/catalog` | Multi-board feed + catalog |
| 2 | `prof-2` | `/biz`, `/biz/catalog` | Single board feed + catalog |
| 3 | `prof-3` | `/pol`, `/pol/catalog`, `/g`, `/g/catalog` | Board switching (feed reloads) |
Keep batches balanced. Add thread views (`/:boardIdentifier/thread/:cid`) as needed.
## Step 2: Run Profiler Subagents Sequentially
Read the profiler subagent definition at `.codex/agents/profiler.toml`. Then spawn one `profiler` subagent for the first batch using Codex's current delegation tool:
```
For each batch, create a subagent request:
agent_type: "profiler"
prompt: |
Session name: "prof-N"
Routes to profile: /route1, /route2, ...
Any non-default app URL or extra profiling constraints
```
Wait for that profiler to close its browser and return results before spawning the next batch. Never run profiler or browser-check subagents concurrently: competing browser sessions both saturate the machine and invalidate timing measurements.
## Step 3: Merge Results
Collect structured output from each subagent and merge:
1. Concatenate all Critical / Warning / React Rerenders / Scroll Jank / Info items
2. Combine per-view summary tables into one
3. Merge react-scan component data across routes (same component appearing in multiple routes = sum counts)
4. Deduplicate shared issues (e.g., same slow resource across routes)
5. Sort by severity (Critical first)
## Step 4: Final Report
```markdown
## Performance Profile Results
### Critical
- [metric]: [value] at [route] — [what likely needs fixing]
### Warning
- [metric]: [value] at [route] — [what likely needs fixing]
### React Rerenders
- [route]: [N] commits during load, [M] during scroll — [likely cause]
- Render bursts detected at [routes] — suggests cascading state updates
- Top rerendering components (react-scan):
- [ComponentName]: [total count] renders across [routes], [time]ms total
- [ComponentName]: [total count] renders across [routes], [time]ms total
### Scroll Jank
- [route]: [N] long tasks during scroll (max [X]ms), [M] React commits — [likely cause]
### Info
- [observations]
### Per-View Summary
| View | Nav (ms) | Long Tasks | CLS | LCP (ms) | Commits | Scroll Commits | Bursts | Top Component |
|------|----------|-----------|-----|-----------|---------|----------------|--------|---------------|
| /all | ... | ... | ... | ... | ... | ... | ... | ... |
```
## Interpreting React Metrics
| Signal | Likely cause | Fix direction |
|--------|-------------|---------------|
| High commits, no long tasks | Frequent cheap rerenders | `React.memo`, stabilize props |
| High commits + long tasks | Expensive rerenders | Profile render cost, split components |
| High scroll commits | Scroll/intersection observer triggering renders | Throttle handlers, memoize list items |
| Render bursts (>5 in 100ms) | Cascading state updates | Batch updates, review Zustand selectors |
| react-scan: component with >30 renders | Missing memoization or unstable references | `useMemo`/`useCallback`, check parent renders |
| react-scan: component with >50ms time | Expensive render function | Split component, move work out of render |
## Element-source follow-up
When `react-scan` identifies a rerender hotspot but you still need the exact file behind a concrete DOM node, hand off to `$inspect-elements`.
```bash
playwright-cli -s=prof-followup eval "async el => JSON.stringify(await window.__ELEMENT_SOURCE__.resolve(el))" e7
```
Use `source.filePath` as the direct edit target and `stack` to understand which parent components own the node.
## Step 5: Cleanup
After profiling is complete and the report is delivered, verify no orphaned processes were left behind:
```bash
# Check for any Vite dev servers started during profiling
ps aux | grep 'vite.*--port' | grep -v grep
```
- If the dev server was already running before Step 0, leave it alone.
- If the orchestrator started the dev server in Step 0, kill it now.
- If there are multiple Vite processes (should never happen), kill the extras and warn the user.
Confirm the profiling session released the shared browser slot:
```bash
./scripts/pw-session.sh status
```
If a failed profiler still owns the slot, close that exact recorded session with `./scripts/pw-session.sh close <session>`. A slot whose browser already died is reclaimed by the next `open`, so it needs no manual cleanup. Never use `close-all` or `kill-all` during concurrent agent work.
## Notes
- **Session isolation**: Each subagent uses a short task-specific playwright-cli session (`-s=prof-<task>-N`).
- **Context isolation**: Each subagent runs in its own context window.
- **Per-route collection**: Data resets on each `goto` — the profiler collects before navigating away.
- **addInitScript persistence**: Instrumentation re-injects automatically in each new document.
- **Tracing**: Each subagent produces a `trace.zip` viewable in [Trace Viewer](https://trace.playwright.dev).
- **Board codes**: `biz`, `pol`, `g`, `a`, `v`, etc. map to community addresses via the directory.
- **Empty react-scan report**: react-scan is a dynamic import, so `__getReactScanReport()` returns `{}` for the first moment after a `goto`. If it is empty, wait ~1s and re-read before falling back to commit counts + render bursts (still useful, just no component names).