inspector-performance
SkillWeb & browsingInspector performance across the whole stack, single-worker backend (SQLite / caching / bucket-I/O / hot paths), frontend (bundle / render / wire-shape), and the live-stack runtime probing that produces the evidence (Playwright/Chrome MCP + Flask/Vite log correlation). The cost model, the measurement playbook, and the cautionary patterns.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the inspector-performance skill
What this skill tells your AI
The instructions your AI receives, as published by qud-technologies/quranic-universal-audio in .claude/skills/inspector-performance/SKILL.md and read by ahel’s review.
Performance skill for the Inspector. Standalone — references split by layer. This replaces the old performance.md rule: same intent (a public web app on one worker, where per-request waste compounds across users), more depth, plus the runtime-probing methodology folded in.
The cost model (internalize this before optimizing anything)
Single-worker gunicorn -w 1, 16 gthreads, 2 vCPU / 18 GB HF Space. Consequences that drive every decision:
- Pure-CPU work serializes across all users. A 1 s CPU spike (a big
orjsondump, a pydantic catalog rebuild, an un-cached classify) blocks all 16 gthreads. Plan for sub-100 ms on per-request hot paths. The GIL means threads only help I/O-bound work (bucket reads, SSL recv release the GIL); CPU work does not parallelize. - Bucket reads are the slowest layer. FUSE mount in prod;
hffs.cat_filefallback is ~50–500× slower when unmounted. Treat each bucket read as expensive; fan out independent reads withThreadPoolExecutor. - SQLite is the source of truth, one serialized writer (
connection._WRITE_LOCK), WAL readers concurrent. State transitions block on a synchronous full-DB upload to the bucket before acking (durable_transaction). - Aggregate is the real number. A 100 ms regression × N concurrent users × M reads/session compounds. The arithmetic that matters is "× N", not the single-call wall-clock.
What changed vs the old rule (don't trust stale framing)
- The old
performance.mdpointed at theinspector-runtime-proberagent as the probing entry point. That agent is gone —performance-expertowns probing now, and the methodology lives inreferences/probing.md. - There is no audio prefetch worker and no GC sweeper (both removed). Don't reason about TTL purge or fetch-warming as live costs.
Hot paths (where the time actually goes)
| Path | Dominant layer | Cold | Warm |
|---|---|---|---|
| Dashboard / public catalog load (the anonymous concurrent path) | Python CPU (pydantic catalog rebuild + orjson dump) | ~38 ms, projected ~700 ms @100× deliveries | dict handback (db_seq-keyed byte cache) |
Segments validate (/api/seg/validate) | cold = bucket I/O (4 parallel reads); warm = CPU classify | 4-read fan-out | cache miss only after save |
Peaks (/api/seg/peaks) | cold = bucket I/O (≤114 gzip shards, 8-way fan-out); warm = dict lookup | ~45 ms cold (mounted) | pre-serialized orjson bytes |
Save (save_seg_data) | bucket I/O (4–5 serialized writes/reads) | — | — |
Audio play (audio-proxy) | network / OS sendfile, ~0 Python CPU on the mount path | — | — |
| State transition | bucket I/O (synchronous full-DB upload, CAS-guarded) | — | — |
Details + file:line in references/backend-db.md.
Reference index
| Reference | When to read | Key files |
|---|---|---|
references/backend-db.md | Compute placement (write vs read), the full cache inventory + invalidation, I/O fan-out + concurrency, save/validate/catalog cost, the two unbounded caches, the bench/drift measurement playbook, local-vs-HF | services/storage/cache.py, services/db/{sync,connection}.py, services/segments/save.py, services/validation/__init__.py, routes/segments/peaks.py, scripts/bench_storage.py |
references/frontend.md | FE critical path, bundle/chunk splitting, the two-layer tab gate, lazy panels, wire-shape, $effect hygiene, audio preload audits, the CSS/JS bundle regrets | frontend/vite.config.ts, src/main.ts, src/App.svelte, tabs/*/stores, lib/refs/quran-refs.ts, lib/utils/peaks-decode.ts |
references/probing.md | Driving the live stack: bring-up commands, exact Playwright/Chrome MCP tool calls + browser_evaluate recipes, browser↔Flask-log correlation, ranked-summary discipline, local-vs-HF for probing | — |
Method (the non-negotiables)
- Measure before guessing. cProfile /
perf_countermarkers /bench_storage.py. Numbers > intuition. - Cold and warm are different problems. Cold = post-save / first-load (bucket I/O). Warm = subsequent interaction (CPU). Optimize them separately; never report one as the other.
- Isolate Python CPU from wall-clock. CDN/HTTPS/disk variance masks CPU. Profile the service function with caches pre-primed (no bucket read) to see the CPU floor.
- Idle-system rule. Take numbers with loadavg < 1; on one worker, background load skews everything.
- Any change to a perf-sensitive path ships with: a drift check (byte-equivalent vs ground-truth snapshot), before/after numbers, and the idle-system caveat. Compute-placement / cache / parallelism / persisted-field-writer changes especially.
Cautionary patterns (the "had we done X first" core)
- Cache invalidation that isn't keyed on a version is hand-maintained and fragile. The DB caches self-invalidate on
db_seq; the bucket-file caches don't, so their invalidation is a choreography insave.pythat's easy to get subtly wrong (the auto-split "only when uid set changes" gate is exactly this). New cache → entry incache.py+ an explicit invalidation hook; never an ad-hoc module global. - Two caches are unbounded (
_PEAKS_CACHE,_AUDIO_CACHE_STATUS) — they grow per (reciter × url) for the process lifetime. Don't add a third; bound new caches (LRU / explicit eviction). - Recompute-on-read of a pure-over-stable-input value is waste. Persist it at write-time behind one source-of-truth helper, drift-gated (qalqala, boundary_adj). Asymmetric save-vs-read ownership is a smell.
- Sequential bucket reads in a hot path are a code smell — check whether each call truly depends on the prior one; fan out if not.
- Wire-shape bloat compounds (~25 B/row × thousands × many users). Strip unused fields aggressively; keep payloads slim (slim int8 peaks,
limit-capped catalog). - Eager work the user may never need — a tab's
onMountfetch behind ahidden=-only gate, apreload="metadata"audio element mounted in a panel, a heavy dep in the main bundle. Defer mount (not just visibility), lazy-load panels, dynamic-import-gate heavy chunks.
Relationship to docs/reference/ (don't create drift)
This skill is a performance lens + practices, not a second copy of subsystem knowledge. The what-is ground truth lives in docs/reference/ and wins on conflict:
- Cache-module mechanics, the invalidation rule, the route map →
docs/reference/architecture.md - SQLite substrate, repos,
sync.py, db_seq →docs/reference/database.md - Validation engine, persisted classifier fields, the bench/drift harness →
docs/reference/validation.md - FE SPA shell, stores, lazy-mount →
docs/reference/frontend.md
What this skill owns (no reference doc does): the cost model, the consolidated cache inventory (perf lens — bounded/unbounded, hit/miss), the hot-path cost map, the measurement/profiling playbook, and the runtime-probing methodology. When cache.py / a hot path / a persisted-field writer changes, update the owning reference doc AND this skill's lens. Don't restate reference mechanics here beyond the one-line anchor needed to make a cost point.
What this skill deliberately doesn't cover
Audio-subsystem internals (see the inspector-audio skill — performance-expert delegates audio-domain reasoning to audio-expert). Deployment/image build (see docs/reference/config-deploy.md). DB schema/migration mechanics beyond their perf characteristics (see docs/reference/database.md).
Signals
- GitHub stars
- 33
- Forks
- 4
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
inspector-performance- Source
- github.com/qud-technologies/quranic-universal-audio