letfixit
v0.2.2
Published
LetFixIt — AI-based realtime performance analysis & suggestions for Flutter, Android & iOS, powered by the local Ultron n0-beta model.
Maintainers
Readme
LetFixIt (ParityFix) — AI-based realtime performance analysis & suggestions
📦 Package of the Day — real-time performance analysis for Flutter, Android & iOS, powered by a private local AI. No cloud. No account. One command.
A lightweight developer tool that connects to a running Flutter, Android, or iOS app, collects real-time performance data, analyzes it with deterministic rules, and shows AI-powered suggestions on a local dashboard at http://localhost:8081.
Smart, low-CPU AI: widgets/views are pre-scored deterministically in code first — only the genuinely expensive or ambiguous ones ever reach the local LLM. Cheap ones are auto-cleared without touching the model, so realtime profiling stays responsive.
Zero-SDK by default. Flutter connects by a launch flag alone — no package to add, no code to call. Android's live monitor and Connected-Device Test work the same way, over adb, on any installed app. A couple of things genuinely need one line of code (they're listed explicitly below) because nothing at the OS level can see them.
Contents
- Install & model setup
- Flutter Quick Start
- Android Quick Start
- Connected Device Performance Test
- AI config & local models
- Dashboard panels & flows
- Limitations
- Roadmap
Install & model setup
From npm (published):
npm install -g letfixit # postinstall runs setup and PROMPTS for the model key
# → paste the access key you were given (input hidden)
letfixit # branded startup → pick a model → analyze menuThe decryption key is never bundled — setup asks for it interactively, so only users you give the key to can decrypt a model. For CI/non-interactive installs, set PARITYFIX_MODEL_KEY in the environment instead of typing it.
From source (clone):
npm install # runs postinstall → sets up the local model automatically
npm start # branded startup → pick a model → analyze menuTwo local models, pick one:
| Model | Kind | Port | Use when |
|---|---|---|---|
| Electro (default) | light, CPU-only | :8082 | You want fast, low-CPU suggestions on any machine |
| Ultron n0-beta | deep reasoning | :8080 | You want more thorough advice and can spare more CPU/RAM |
🎯 Which model do I want?
- Fast, low-CPU, works fine on any machine → Electro (default, no config needed)
- Deeper, more thorough advice, can spare more CPU/RAM → Ultron n0-beta (
ACTIVE_MODEL=ultron)Not sure? Start with Electro — switch to Ultron later with zero re-download of anything else (
ACTIVE_MODEL=ultronin.env, or pick it innpm start's menu).
Both install through the identical encrypted-chunk flow with the same decryption key. Switch anytime: ACTIVE_MODEL=electro or ACTIVE_MODEL=ultron in .env, or letfixit setup electro|ultron|all. npm start's interactive menu also lets you pick per-run.
The local model ships as N AES-256-GCM-encrypted chunks of ≤300 MB each (a ~2.6 GB model → ~10 chunks). Chunks can live on multiple hosts (each has a primary url + optional mirrors), and downloads are resumable — a dropped connection continues from where it left off (HTTP Range), tries mirrors in order, and retries per chunk. Setup downloads (live progress bar), verifies each SHA-256, decrypts, merges, verifies the full-model checksum, and finalizes ~/.letfixit/models/<model>/*.gguf.
Setup is non-fatal: if the chunk manifest or key isn't configured it prints instructions and continues (cloud fallback still works). Retry any time with npm run setup.
Two things must be present for the automatic download:
config/<model>.manifest.json— chunk URLs + IVs/checksums.PARITYFIX_MODEL_KEY— the decryption passphrase (env var). Never commit it.
Package updates don't re-download the model. It lives in ~/.letfixit/models/<model>/, outside this package's own install directory entirely — so an npm update never touches it. postinstall compares a small version marker against the current release's checksum and only re-fetches if the model itself actually changed; otherwise it's a no-op. Uninstalling the package doesn't delete it either (same reasoning: npm can't tell "update" apart from "real uninstall," so nothing automatic touches it) — remove it explicitly with letfixit clean / npm run clean if you want it gone.
Maintainers — producing a release (from a source .gguf):
PARITYFIX_MODEL_KEY='your-strong-passphrase' npm run encrypt-model -- --model electro path/to/model.gguf
# → dist/model-chunks/chunk.{0,1,2}.enc + <model>.manifest.json
# Upload the chunks (Drive / GitHub Release), paste their URLs into the manifest,
# copy it to config/<model>.manifest.json, and store the key as a GitHub Actions secret.Security note: a decryption key shipped inside a public package is only obfuscation, not real protection. The key is kept out of the repo (env var + CI secret); for true gating you'd deliver it from a backend at install time. The chunking + encryption here raises the bar and keeps the weights off the public repo, which is the practical goal for now.
Flutter Quick Start (zero-SDK)
No package to add, no code to call. Just launch your app with the VM service open:
flutter run --vm-service-port=8181 --disable-service-auth-codesStart the dashboard (npm start) and either let it auto-discover your app (it polls for Flutter VMs and offers a one-click connect banner) or paste the URL manually if you disabled auth codes on a different port. Data — frames, memory, widget rebuilds with exact file:line, HTTP calls, UI-thread CPU — starts flowing immediately, all read directly off the Dart VM Service.
This covers everything, including API/network calls (ext.dart.io.getHttpProfile, polled automatically) — there's no separate HTTP client to swap in.
Android Quick Start
Android splits cleanly into what's automatic and what needs one line of code, because some things (network traffic, Compose recomposition counts) simply aren't visible from outside your app process — there's no OS-level equivalent to Flutter's VM Service for them.
Works with zero code, once you call PerfAnalyzer.start() once in your Application:
class MyApp : Application() {
override fun onCreate() {
super.onCreate()
PerfAnalyzer.start(this)
}
}// build.gradle
dependencies {
implementation project(':perf_analyzer') // adapters/android
}- Frames, memory, crashes (Choreographer +
dumpsys-equivalent + logcat) - View/XML overdraw — auto-attaches to every Activity via
ActivityLifecycleCallbacks, walks the View tree, flags stacked-background overdraw by resource-id. Nothing to add per-screen.
One line, when you need it (nothing else works without adding this — there's no way around it, explained below):
// API/network calls — adb can't see network traffic, only your app can report it:
val client = OkHttpClient.Builder().addInterceptor(PerfInterceptor()).build()
// Jetpack Compose recomposition counts + file:line — Compose's internals are
// closed off to anything outside the app (that's Android Studio's own private
// protocol), so this is the only way to get per-composable data:
Text("hi", modifier = Modifier.perfTrack("Greeting"))Prefer not to touch your app's code at all? Use the live debug monitor below — it gets frames/memory/crashes/overdraw/CPU with genuinely zero code changes, at the cost of not seeing API calls or Compose recomposition.
Android native — live debug monitor (over adb, no SDK)
Attach to a running app over adb and stream metrics in real time — no in-app SDK, no code changes, no debuggable build even required for most metrics. Great for a debug build you're actively exercising, or for profiling someone else's installed app.
Requirements: adb on PATH + a device/emulator with USB debugging enabled, and the app already running (or about to be).
Run it: npm start → pick the Android live monitor from the menu, choose the device, confirm the package (the current foreground app is offered as the default), then open the dashboard and flip the platform toggle to Android. Live panels populate from:
- Frames —
dumpsys gfxinfo <pkg> framestats→ FPS strip, build-vs-raster frame chart, jank. - Memory —
dumpsys meminfo <pkg>(TOTAL PSS). - CPU —
dumpsys cpuinfo(whole-process %, not isolated to the UI thread — see the in-app tooltip for the exact caveat). - Overdraw — stacked-background heuristic on whatever View resource-ids are on screen.
- Errors —
logcat --pid=<pid> *:E(FATAL EXCEPTION / ANR flagged critical).
The passive rule engine runs on this stream too, so jank / overdraw / CPU-busy / memory-growth / crash-pattern findings appear live in the Junk Feed. API calls still need PerfInterceptor (see above) — this is the one thing adb fundamentally cannot observe.
Connected Device Performance Test (Android & iOS)
Profile any installed app on a device plugged into your laptop — no SDK integration, no debuggable build. Launch → perform a flow → capture → restart, repeated for N iterations, then one aggregated report.
Two ways to run it:
- In the dashboard (recommended) — open the Device Review tab: pick a connected device (Android + iOS listed together, tagged by platform and GPU tier where recognized), enter the package/bundle id and a flow name, then hit Capture once per iteration (replaces the old "press Enter" — perform the flow on-device, click Capture, it relaunches for the next run automatically). After all iterations, Generate report gives you:
- A 0–100 score plus per-parameter sub-scores.
- A worst-offender card — the single parameter furthest past its own health limit, with which iteration produced it — computed in code, so the AI never has to guess at ranking.
- Per-iteration trend charts for jank/p90/p99/memory/CPU, each with its universal limit drawn as a reference line — so a chart shows where the value actually sits, not just a number.
- Build-vs-raster breakdown (Android) — is the worst run UI-thread-bound or GPU-bound, from real per-frame
framestats. - Worst-first AI suggestions — the local model addresses the worst offender first, with the universal limits stated explicitly in its context.
- CLI wizard (
npm start→ "Connected Device Performance Test") — same underlying engine, readline-driven, produces a self-contained HTML report (Chart.js) instead of the dashboard view. Useful headless/CI, or on iOS where the dashboard flow shares the same capture path anyway.
Every parameter is judged against its own universal limit (jank <5%, p90 <16.6ms, p99 <40ms, memory growth <10MB, peak memory <250MB, CPU <40%) — not a specific target device. That's deliberate: the report tells you whether this run is healthy on its own terms, regardless of which phone happened to be plugged in.
Requirements:
- Android:
adbon PATH (platform-tools) + a device with USB debugging enabled & authorized. (ADB_PATHin.envoverrides the adb location.) - iOS: Xcode command-line tools (
xcrun); a paired device or a booted Simulator.
iOS caveats (pre-existing in the underlying capture, not new): frame timing comes from an Instruments Core Animation trace, which has no per-frame build/raster split (that chart just doesn't render for iOS) and no memory/CPU capture at all today (both show n/a) — the xctrace export schema also varies by Xcode version, so parseFpsExport() in ios_bridge.js is the most likely spot to need a small tweak on a given machine.
Module Performance Test (Flutter widgets & Android views/composables)
The one place AI runs live, sort of: pick a screen/flow, exercise it across ≥5 runs, then Analyze with AI unlocks a per-widget report projected onto every known device GPU tier (entry through flagship). Widgets/views are classified by a fixed vocabulary — Flutter's built-in widget types, or Android View class names (WebView, RecyclerView, CardView, etc.) — with known GPU-cost tiers and per-tier risk notes. Jetpack Compose composables have no such fixed vocabulary from outside the app, so they classify as "unknown cost" unless tagged; the AI still sees their recomposition rate and reasons about that.
Only the ambiguous/expensive widgets ever reach the model (code-first triage) — a clean module often skips the LLM call entirely.
AI Config (all optional)
Copy .env.example to .env and fill in the keys you have. All are optional — the dashboard works without any AI configured (you just get code-computed findings, no natural-language advice).
GROQ_API_KEY=gsk_...
GEMINI_API_KEY=AIza...
CLAUDE_API_KEY=sk-ant-...
ENABLE_LOCAL_AI=true
ACTIVE_MODEL=electro # or: ultron
PARITYFIX_MODEL_KEY=... # the local-model decryption keyAI routing order for ad-hoc findings: local model → Groq → Gemini → Claude. The first available provider wins. (The live dashboard never calls AI per-finding regardless of provider — see "Dashboard panels & flows" below.)
To skip local AI entirely, set ENABLE_LOCAL_AI=false — the server routes straight to your configured cloud key.
Dashboard panels & flows
The dashboard has three top-level views:
| View | What it's for | |---|---| | Live Monitor | Real-time panels below — no AI, ever. Findings are plain rule-based observations. | | Module Test | Record ≥5 runs of one flow, get an AI-analyzed per-widget/view report across device tiers. | | Device Review | Connected-device iteration loop (adb/xcrun) → aggregated report, universal limits, worst-offender + AI. |
🧭 Which flow do I want?
- Actively debugging right now, want live numbers as you interact with the app? → Live Monitor
- Want a scored, AI-reviewed report for one screen/flow, comparable across device tiers? → Module Test
- Profiling any installed app on a plugged-in device, no debug build, no code changes at all? → Device Review
Live Monitor panels:
| Panel | Platform | What it shows |
|---|---|---|
| FPS Strip | Both | Rolling 60-frame bar chart, current FPS, jank count |
| Frame Timeline | Both | Build vs raster breakdown per frame, against the 16.6ms budget |
| Memory Monitor | Both | Heap over time, growth-rate badge |
| UI Thread CPU | Both | Busy % + top consumers (Flutter: per-function; Android: whole-process, see caveat above) |
| Error Feed | Both | Exceptions and crashes, newest first |
| API Monitor | Both | Endpoint, method, payload size, duration, status — needs PerfInterceptor on Android |
| Rebuild Tracker | Both | Widget rebuilds/s (Flutter) or Compose recompositions/s via Modifier.perfTrack (Android), with file:line |
| Overdraw Map | Android | Real stacked-background heuristic by view, auto-populated — no code needed |
| Widget Tree | Flutter | Live widget hierarchy with per-node GPU cost + creation location |
| AI Suggestion Cards | Both | Shown only in Module Test / Device Review reports — CAUSE / FIX / CODE / GAIN per finding |
Current Limitations
- Flutter connection needs a debug/profile build with the VM service open (
--vm-service-port) — not usable against a release build. - Android's
Modifier.perfTrackrequires your app's Kotlin version to be ~1.9.20+ (or Kotlin 2.0+ with the newer Compose compiler plugin) to build the adapter module at all, even if you never call it. - Android CPU% (both live monitor and any future device-review CPU capture) is whole-process, not isolated to the UI thread — a true per-thread number needs
top -H -p <pid>, not yet implemented. - iOS Connected-Device Test has no memory/CPU capture and no build/raster split (see caveats above).
matchDeviceModelGPU-tier lookup is more reliable on iOS (xctracereports human-readable names) than Android (adboften reports cryptic model codes likeSM-S928Binstead of marketing names) — expect frequent "unknown tier" on real Android hardware.- Local AI quality depends on quantization; cloud APIs give more consistent suggestions.
- No persistence — findings and sessions reset when the server restarts.
Roadmap
Shipped this cycle: Android View/Compose auto-detection (overdraw heuristic + opt-in recomposition tracking), Android AI classifier + worst-offender/build-vs-raster analysis for Module Test, Connected Device Review folded into the dashboard (was CLI-only), two-model registry (Electro + Ultron).
Next:
- [ ] True per-thread Android CPU (
top -H) instead of whole-process % - [ ]
device_gpu_db.jsonmodel-code → marketing-name alias table for Android tier matching - [ ] iOS memory/CPU capture in Connected Device Review
- [ ] Persistent SQLite finding history
- [ ] pub.dev / Maven Central release
- [ ] CI integration — fail build on critical findings
