letfixit 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/.env.example +33 -0
  2. package/README.md +226 -0
  3. package/adapters/android/build.gradle +28 -0
  4. package/adapters/android/src/main/kotlin/dev/perfanalyzer/ChoreographerAdapter.kt +43 -0
  5. package/adapters/android/src/main/kotlin/dev/perfanalyzer/LogcatReader.kt +64 -0
  6. package/adapters/android/src/main/kotlin/dev/perfanalyzer/MemoryObserver.kt +46 -0
  7. package/adapters/android/src/main/kotlin/dev/perfanalyzer/OkHttpInterceptor.kt +53 -0
  8. package/adapters/android/src/main/kotlin/dev/perfanalyzer/PerfAnalyzer.kt +100 -0
  9. package/adapters/flutter/lib/perf_analyzer.dart +73 -0
  10. package/adapters/flutter/lib/src/frame_observer.dart +45 -0
  11. package/adapters/flutter/lib/src/network_observer.dart +72 -0
  12. package/adapters/flutter/lib/src/rebuild_observer.dart +57 -0
  13. package/adapters/flutter/lib/src/universal_event.dart +50 -0
  14. package/adapters/flutter/lib/src/vm_service_adapter.dart +81 -0
  15. package/adapters/flutter/pubspec.lock +184 -0
  16. package/adapters/flutter/pubspec.yaml +13 -0
  17. package/config/model.manifest.example.json +108 -0
  18. package/config/model.manifest.json +108 -0
  19. package/core/schema/universal_event.dart +63 -0
  20. package/dashboard/dist/assets/index-D5TCSsvB.js +107 -0
  21. package/dashboard/dist/index.html +118 -0
  22. package/package.json +62 -0
  23. package/scripts/encrypt-model.js +86 -0
  24. package/scripts/fill-manifest.js +67 -0
  25. package/scripts/load-env.js +20 -0
  26. package/scripts/model_crypto.js +14 -0
  27. package/scripts/setup-model.js +269 -0
  28. package/server/adb_bridge.js +201 -0
  29. package/server/adb_score.js +77 -0
  30. package/server/ai_proxy.js +373 -0
  31. package/server/analysis_engine.js +222 -0
  32. package/server/android_bridge.js +124 -0
  33. package/server/android_live.js +167 -0
  34. package/server/data/device_gpu_db.json +465 -0
  35. package/server/device_db.js +94 -0
  36. package/server/device_review.js +146 -0
  37. package/server/finding_generator.js +56 -0
  38. package/server/flutter_bridge.js +414 -0
  39. package/server/index.js +619 -0
  40. package/server/ios_bridge.js +181 -0
  41. package/server/model_config.js +54 -0
  42. package/server/report_template.js +169 -0
  43. package/server/session_manager.js +370 -0
  44. package/server/widget_gpu_classifier.js +77 -0
  45. package/server/widget_triage.js +78 -0
  46. package/setup.sh +160 -0
package/.env.example ADDED
@@ -0,0 +1,33 @@
1
+ # ── Cloud AI fallback keys (optional — Nemotron runs locally by default) ──────
2
+ # Get a free Groq key at console.groq.com — fastest cloud fallback
3
+ GROQ_API_KEY=
4
+ GEMINI_API_KEY=
5
+ CLAUDE_API_KEY=
6
+
7
+ # ── Local AI ───────────────────────────────────────────────────────────────────
8
+ ENABLE_LOCAL_AI=true
9
+ LOCAL_AI_PORT=8080
10
+
11
+ # ── Dashboard ──────────────────────────────────────────────────────────────────
12
+ DASHBOARD_PORT=8081
13
+
14
+ # ── Flutter VM Service ─────────────────────────────────────────────────────────
15
+ # Set this to skip the port prompt on `npm start`.
16
+ # Run Flutter with: flutter run --vm-service-port=8181 --disable-service-auth-codes
17
+ FLUTTER_VM_URL=
18
+
19
+ # ── Ultron n0-beta model download (encrypted chunks) ───────────────────────────
20
+ # Decryption passphrase for the model chunks. NEVER commit this — set it locally
21
+ # and store it as a GitHub Actions secret for CI releases. Without it, `npm run
22
+ # setup` skips the automatic download and prints instructions.
23
+ PARITYFIX_MODEL_KEY=
24
+ # Optional: path to a chunk manifest (defaults to config/model.manifest.json).
25
+ PARITYFIX_MODEL_MANIFEST=
26
+ # Optional: max size per encrypted chunk in MB (default 300). Only affects the
27
+ # maintainer's `npm run encrypt-model` step — install reads whatever the manifest lists.
28
+ PARITYFIX_CHUNK_MB=300
29
+
30
+ # ── Team model cache (optional, legacy setup.sh path) ──────────────────────────
31
+ # Upload the model to Google Drive, share publicly (Anyone → Viewer), then paste
32
+ # the file ID here. Used by ./setup.sh (HuggingFace fallback path).
33
+ GDRIVE_MODEL_ID=
package/README.md ADDED
@@ -0,0 +1,226 @@
1
+ # ParityFix v0.1.0 — AI-based realtime performance analysis & suggestions
2
+
3
+ A lightweight developer tool that connects to a running Flutter or Android app, collects real-time performance data, analyzes it with configurable rules, and shows AI-powered suggestions on a local dashboard at `http://localhost:7000`.
4
+
5
+ **Smart, low-CPU AI:** widgets are pre-scored deterministically in code first — only the genuinely expensive or ambiguous ones are escalated to the local LLM. Cheap widgets are auto-cleared without ever touching the model, and the local model is capped to half your CPU cores, so realtime profiling stays responsive.
6
+
7
+ ---
8
+
9
+ ## Install & model setup
10
+
11
+ **From npm (published):**
12
+ ```bash
13
+ npm install -g letfixit # or: npx letfixit
14
+ export PARITYFIX_MODEL_KEY=… # the decryption passphrase (enables the local model)
15
+ letfixit # branded startup → "Ultron n0-beta initializing…" → analyze menu
16
+ ```
17
+
18
+ **From source (clone):**
19
+ ```bash
20
+ npm install # runs postinstall → sets up the local model automatically
21
+ npm start # branded startup → "Ultron n0-beta initializing…" → analyze menu
22
+ ```
23
+
24
+ The chunk manifest ships inside the package, so installs only need `PARITYFIX_MODEL_KEY`
25
+ to fetch + decrypt the weights. Without it, ParityFix still runs (cloud-fallback AI, or
26
+ no suggestions) — the model download is skipped, never fatal.
27
+
28
+ The local model — **Ultron n0-beta** — 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 chunk 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. On install, ParityFix downloads them (live progress bar), verifies each SHA-256, decrypts, merges, verifies the full-model checksum, and finalizes `bin/ultron/ultron-n0-beta.gguf`, then prints the model info + a short reliability disclaimer.
29
+
30
+ Chunk size is configurable via `PARITYFIX_CHUNK_MB` (default 300) at encrypt time.
31
+
32
+ Setup is **non-fatal**: if the chunk manifest or key isn't configured it prints instructions and continues (you can still use cloud fallback or `./setup.sh`). Retry any time with `npm run setup`.
33
+
34
+ **Two things must be present for the automatic download:**
35
+ - `config/model.manifest.json` — chunk URLs + IVs/checksums. Copy `config/model.manifest.example.json` and fill it in.
36
+ - `PARITYFIX_MODEL_KEY` — the decryption passphrase (env var). **Never commit it.**
37
+
38
+ **Maintainers — producing a release** (from a source `.gguf`):
39
+ ```bash
40
+ PARITYFIX_MODEL_KEY='your-strong-passphrase' npm run encrypt-model -- path/to/model.gguf
41
+ # → dist/model-chunks/chunk.{0,1,2}.enc + model.manifest.json
42
+ # Upload the chunks (Drive / GitHub Release), paste their URLs into the manifest,
43
+ # copy it to config/model.manifest.json, and store the key as a GitHub Actions secret.
44
+ ```
45
+
46
+ > **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.
47
+
48
+ ---
49
+
50
+ ## Flutter Quick Start
51
+
52
+ **1. Add the package** to your `pubspec.yaml`:
53
+ ```yaml
54
+ dependencies:
55
+ perf_analyzer:
56
+ path: /path/to/perf_analyzer/adapters/flutter
57
+ ```
58
+
59
+ **2. Start the bridge server** (run once, keep it running):
60
+ ```bash
61
+ npm start
62
+ ```
63
+
64
+ **3. Call `start()` in your `main()`**:
65
+ ```dart
66
+ import 'package:perf_analyzer/perf_analyzer.dart';
67
+
68
+ Future<void> main() async {
69
+ WidgetsFlutterBinding.ensureInitialized();
70
+ await PerfAnalyzer.start();
71
+ runApp(const MyApp());
72
+ }
73
+ ```
74
+
75
+ **4. Launch your app** with the VM service flag:
76
+ ```bash
77
+ flutter run --enable-vmservice
78
+ ```
79
+
80
+ Open `http://localhost:7000` — data starts flowing immediately.
81
+
82
+ ---
83
+
84
+ ## Android Quick Start
85
+
86
+ **1. Add the dependency** to your app `build.gradle`:
87
+ ```groovy
88
+ dependencies {
89
+ implementation project(':perf_analyzer')
90
+ }
91
+ ```
92
+
93
+ **2. Start the bridge server**:
94
+ ```bash
95
+ npm start
96
+ ```
97
+
98
+ **3. Initialize in your `Application` class**:
99
+ ```kotlin
100
+ class MyApp : Application() {
101
+ override fun onCreate() {
102
+ super.onCreate()
103
+ PerfAnalyzer.start(this)
104
+ }
105
+ }
106
+ ```
107
+
108
+ **4. (Optional) Add the OkHttp interceptor** to capture API calls:
109
+ ```kotlin
110
+ val client = OkHttpClient.Builder()
111
+ .addInterceptor(PerfInterceptor())
112
+ .build()
113
+ ```
114
+
115
+ ---
116
+
117
+ ## Android native — live debug monitor (over adb, no SDK)
118
+
119
+ The Android counterpart to Flutter's live dashboard: attach to a **running app over adb** and stream metrics in real time — **no in-app SDK, no code changes**. Great for a debug build you're actively exercising.
120
+
121
+ **Requirements:** `adb` on PATH + a device/emulator with USB debugging enabled, and the app already running (or about to be).
122
+
123
+ **Run it:**
124
+ ```bash
125
+ npm start # then choose option 2 — "Android native — live debug monitor"
126
+ ```
127
+
128
+ Pick the device, confirm the package (the currently-foreground app is offered as the default), then open the dashboard and flip the platform toggle to **Android**. Live panels populate from:
129
+ - **Frames** — `dumpsys gfxinfo <pkg> framestats` parsed into real per-frame timings → FPS strip, frame-time chart (build vs raster), jank.
130
+ - **Memory** — `dumpsys meminfo <pkg>` (TOTAL PSS) → live memory chart.
131
+ - **Errors** — `logcat --pid=<pid> *:E` → crashes/exceptions in the error feed (FATAL EXCEPTION / ANR flagged critical).
132
+
133
+ The passive rule engine runs on this stream too, so jank / memory-growth / crash-pattern findings appear live in the Junk Feed. (API calls need the OkHttp interceptor — see the SDK section — since adb can't observe them.)
134
+
135
+ ---
136
+
137
+ ## Connected Device Performance Test (Android & iOS — no debug build needed)
138
+
139
+ Profile **any installed app** on a mobile plugged into the laptop — no SDK integration, no debuggable build. The laptop drives a launch → perform-flow → restart loop and analyzes GPU / render / raster.
140
+
141
+ - **Android** — reads the OS-side `dumpsys` counters (`gfxinfo`, `meminfo`, `cpuinfo`) Android keeps for every process. Captures render (jank, frame percentiles), memory (PSS), and CPU.
142
+ - **iOS** (physical device via `xcrun devicectl`, or the Simulator via `xcrun simctl`) — records an Instruments **Core Animation** trace per iteration (`xcrun xctrace`) and derives FPS → frame-time percentiles + jank. Memory/CPU show **n/a** (the render template doesn't carry them).
143
+
144
+ **Requirements:**
145
+ - Android: `adb` on PATH (platform-tools) + a device with **USB debugging** enabled & authorized. (`ADB_PATH` in `.env` overrides the adb location.)
146
+ - iOS: Xcode command-line tools (`xcrun`); a paired device or a booted Simulator.
147
+
148
+ **Run it:**
149
+ ```bash
150
+ npm start # then choose option 2 — "Connected Device Performance Test"
151
+ ```
152
+
153
+ The flow:
154
+ 1. Pick a device — Android + iOS devices/simulators are merged into one list, tagged by platform.
155
+ 2. Enter the app **bundle id** (`com.example.app` / `com.example.MyApp`) and a **module name** (`Login Flow`).
156
+ 3. Choose iteration count (default **8**). Each iteration the app launches, **you perform the flow**, then press **Enter** to capture — stats are reset per run. The app is force-stopped/restarted between runs.
157
+ 4. Metrics are **scored in code** (0–100; the overall renormalizes over whichever of render/memory/CPU were captured), issues detected, and — only if issues exist — the **local LLM** suggests concrete platform-specific fixes.
158
+ 5. A self-contained **HTML report** (Chart.js charts, score, suggestions, metrics table) is written to `server/reports/` and opened in your browser. Also served at `http://localhost:8081/reports/<file>`.
159
+
160
+ > **iOS note:** the `xctrace` capture + trace-export parsing follows the Xcode 15+ CLI but hasn't been validated against live hardware from this build — the export schema varies by Xcode version, so `parseFpsExport()` in `ios_bridge.js` is the most likely spot to need a small tweak on your machine. If a trace yields no FPS data, iOS metrics degrade to `n/a` and the report still renders.
161
+
162
+ ---
163
+
164
+ ## AI Config (all optional)
165
+
166
+ Copy `.env.example` to `.env` and fill in the keys you have. All are optional — the dashboard works without any AI configured.
167
+
168
+ ```env
169
+ GROQ_API_KEY=gsk_...
170
+ GEMINI_API_KEY=AIza...
171
+ CLAUDE_API_KEY=sk-ant-...
172
+ ENABLE_LOCAL_AI=true
173
+ LOCAL_AI_PORT=8080
174
+ ```
175
+
176
+ AI routing order: **SmolLM2 (local)** → **Groq** → **Gemini** → **Claude**. The first available provider wins.
177
+
178
+ ---
179
+
180
+ ## Local AI Setup (SmolLM2 360M — zero dependencies)
181
+
182
+ `./setup.sh` handles everything automatically. It downloads a single self-contained [llamafile](https://github.com/Mozilla-Ocho/llamafile) (~350MB, once) to `bin/smollm/smollm2.llamafile`. No external runtimes, no Homebrew, no CUDA drivers needed.
183
+
184
+ ```bash
185
+ ./setup.sh # downloads SmolLM2 if not present
186
+ npm start # starts SmolLM2 on :8080 + bridge server on :7000
187
+ ```
188
+
189
+ `npm start` auto-spawns the llamafile in the background and kills it on `Ctrl+C`.
190
+
191
+ To skip local AI entirely, set `ENABLE_LOCAL_AI=false` in `.env` — the server will route directly to your configured cloud API key.
192
+
193
+ ---
194
+
195
+ ## Dashboard Panels
196
+
197
+ | Panel | Platform | What it shows |
198
+ |---|---|---|
199
+ | **FPS Strip** | Both | Rolling 60-frame bar chart, current FPS, jank count |
200
+ | **Memory Monitor** | Both | Heap over time (line chart), growth rate badge |
201
+ | **Error Feed** | Both | Exceptions and crashes, newest first |
202
+ | **API Monitor** | Both | Endpoint, method, payload size, duration, status |
203
+ | **Rebuild Tracker** | Flutter | Widget rebuild counts/s, sorted by frequency |
204
+ | **Overdraw Map** | Android | View invalidation heatmap |
205
+ | **AI Suggestion Cards** | Both | CAUSE / FIX / CODE / GAIN per finding |
206
+
207
+ ---
208
+
209
+ ## V0 Limitations
210
+
211
+ - Flutter adapter requires `--enable-vmservice` flag (not for production builds).
212
+ - Android WebSocket adapter uses `javax.websocket` — add the dependency manually for apps targeting API < 26.
213
+ - Local AI quality depends on quantization; cloud APIs give better suggestions.
214
+ - No persistence — findings reset when the server restarts.
215
+ - iOS not yet supported.
216
+
217
+ ---
218
+
219
+ ## V1 Roadmap
220
+
221
+ - [ ] iOS adapter (Xcode Instruments bridge)
222
+ - [ ] Persistent SQLite finding history
223
+ - [ ] pub.dev / Maven Central release
224
+ - [ ] `flutter pub run perf_analyzer` CLI setup command
225
+ - [ ] CI integration — fail build on critical findings
226
+ - [ ] Flame chart overlay for frame deep-dives
@@ -0,0 +1,28 @@
1
+ plugins {
2
+ id 'com.android.library'
3
+ id 'org.jetbrains.kotlin.android'
4
+ }
5
+
6
+ android {
7
+ namespace 'dev.perfanalyzer'
8
+ compileSdk 34
9
+
10
+ defaultConfig {
11
+ minSdk 21
12
+ targetSdk 34
13
+ }
14
+
15
+ compileOptions {
16
+ sourceCompatibility JavaVersion.VERSION_1_8
17
+ targetCompatibility JavaVersion.VERSION_1_8
18
+ }
19
+
20
+ kotlinOptions {
21
+ jvmTarget = '1.8'
22
+ }
23
+ }
24
+
25
+ dependencies {
26
+ implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3'
27
+ compileOnly 'com.squareup.okhttp3:okhttp:4.12.0'
28
+ }
@@ -0,0 +1,43 @@
1
+ package dev.perfanalyzer
2
+
3
+ import android.view.Choreographer
4
+ import org.json.JSONObject
5
+
6
+ class ChoreographerAdapter(private val onEvent: (JSONObject) -> Unit) : Choreographer.FrameCallback {
7
+ private var lastFrameTimeNanos: Long = 0
8
+ private var running = false
9
+
10
+ fun start() {
11
+ running = true
12
+ Choreographer.getInstance().postFrameCallback(this)
13
+ }
14
+
15
+ fun stop() {
16
+ running = false
17
+ }
18
+
19
+ override fun doFrame(frameTimeNanos: Long) {
20
+ if (!running) return
21
+
22
+ if (lastFrameTimeNanos != 0L) {
23
+ val durationMs = (frameTimeNanos - lastFrameTimeNanos) / 1_000_000.0
24
+ val severity = when {
25
+ durationMs > 33 -> "critical"
26
+ durationMs > 16.6 -> "high"
27
+ else -> "low"
28
+ }
29
+ val event = JSONObject().apply {
30
+ put("platform", "android")
31
+ put("event", "frame")
32
+ put("timestamp", System.currentTimeMillis())
33
+ put("severity", severity)
34
+ put("type", "frame")
35
+ put("durationMs", durationMs)
36
+ }
37
+ onEvent(event)
38
+ }
39
+
40
+ lastFrameTimeNanos = frameTimeNanos
41
+ Choreographer.getInstance().postFrameCallback(this)
42
+ }
43
+ }
@@ -0,0 +1,64 @@
1
+ package dev.perfanalyzer
2
+
3
+ import android.util.Log
4
+ import org.json.JSONObject
5
+ import java.io.BufferedReader
6
+ import java.io.InputStreamReader
7
+
8
+ class LogcatReader(private val onEvent: (JSONObject) -> Unit) {
9
+ private val tag = "PerfAnalyzer.LogcatReader"
10
+ private var process: Process? = null
11
+ private var thread: Thread? = null
12
+ private var running = false
13
+
14
+ fun start() {
15
+ running = true
16
+ thread = Thread {
17
+ try {
18
+ process = Runtime.getRuntime().exec(arrayOf("logcat", "-v", "time", "-s", "AndroidRuntime:E", "*:F"))
19
+ val reader = BufferedReader(InputStreamReader(process!!.inputStream))
20
+ var line: String?
21
+ while (running && reader.readLine().also { line = it } != null) {
22
+ line?.let { parseLine(it) }
23
+ }
24
+ } catch (e: Exception) {
25
+ Log.w(tag, "Logcat reader stopped: ${e.message}")
26
+ }
27
+ }
28
+ thread!!.isDaemon = true
29
+ thread!!.start()
30
+ }
31
+
32
+ fun stop() {
33
+ running = false
34
+ process?.destroy()
35
+ thread?.interrupt()
36
+ }
37
+
38
+ private fun parseLine(line: String) {
39
+ val isException = line.contains("Exception") || line.contains("FATAL EXCEPTION")
40
+ val isError = line.contains("Error:") && !line.contains("No error")
41
+ if (!isException && !isError) return
42
+
43
+ val fatal = line.contains("FATAL")
44
+ val className = extractClassName(line)
45
+ val message = line.trim()
46
+
47
+ val event = JSONObject().apply {
48
+ put("type", "error")
49
+ put("platform", "android")
50
+ put("timestamp", System.currentTimeMillis())
51
+ put("severity", if (fatal) "critical" else "high")
52
+ put("fatal", fatal)
53
+ put("className", className)
54
+ put("message", message.take(200))
55
+ }
56
+
57
+ onEvent(event)
58
+ }
59
+
60
+ private fun extractClassName(line: String): String {
61
+ val exceptionPattern = Regex("""([A-Za-z.]+Exception|[A-Za-z.]+Error)""")
62
+ return exceptionPattern.find(line)?.value?.substringAfterLast('.') ?: "UnknownError"
63
+ }
64
+ }
@@ -0,0 +1,46 @@
1
+ package dev.perfanalyzer
2
+
3
+ import android.os.Debug
4
+ import org.json.JSONObject
5
+ import java.util.concurrent.Executors
6
+ import java.util.concurrent.ScheduledFuture
7
+ import java.util.concurrent.TimeUnit
8
+
9
+ class MemoryObserver(private val onEvent: (JSONObject) -> Unit) {
10
+ private val scheduler = Executors.newSingleThreadScheduledExecutor()
11
+ private var future: ScheduledFuture<*>? = null
12
+
13
+ fun start() {
14
+ future = scheduler.scheduleAtFixedRate({
15
+ val memInfo = Debug.MemoryInfo()
16
+ Debug.getMemoryInfo(memInfo)
17
+
18
+ val totalPss = memInfo.totalPss
19
+ val dalvikHeap = memInfo.dalvikPss
20
+ val nativeHeap = memInfo.nativePss
21
+
22
+ val severity = when {
23
+ totalPss > 300_000 -> "critical"
24
+ totalPss > 150_000 -> "high"
25
+ else -> "low"
26
+ }
27
+
28
+ val event = JSONObject().apply {
29
+ put("type", "memory")
30
+ put("platform", "android")
31
+ put("timestamp", System.currentTimeMillis())
32
+ put("severity", severity)
33
+ put("totalPss", totalPss)
34
+ put("dalvikHeap", dalvikHeap)
35
+ put("nativeHeap", nativeHeap)
36
+ }
37
+
38
+ onEvent(event)
39
+ }, 0, 2, TimeUnit.SECONDS)
40
+ }
41
+
42
+ fun stop() {
43
+ future?.cancel(false)
44
+ scheduler.shutdown()
45
+ }
46
+ }
@@ -0,0 +1,53 @@
1
+ package dev.perfanalyzer
2
+
3
+ import okhttp3.Interceptor
4
+ import okhttp3.Response
5
+ import org.json.JSONObject
6
+
7
+ class PerfInterceptor : Interceptor {
8
+ override fun intercept(chain: Interceptor.Chain): Response {
9
+ val request = chain.request()
10
+ val start = System.currentTimeMillis()
11
+
12
+ val requestBodySize = try {
13
+ val copy = request.newBuilder().build()
14
+ val buffer = okio.Buffer()
15
+ copy.body?.writeTo(buffer)
16
+ buffer.size
17
+ } catch (_: Exception) {
18
+ 0L
19
+ }
20
+
21
+ val response = chain.proceed(request)
22
+ val durationMs = System.currentTimeMillis() - start
23
+
24
+ val responseBody = response.body
25
+ val responseBytes = responseBody?.bytes() ?: ByteArray(0)
26
+ val responseBodySize = responseBytes.size.toLong()
27
+
28
+ val severity = when {
29
+ responseBodySize > 512000 -> "high"
30
+ durationMs > 500 -> "medium"
31
+ else -> "low"
32
+ }
33
+
34
+ val event = JSONObject().apply {
35
+ put("type", "api")
36
+ put("platform", "android")
37
+ put("timestamp", System.currentTimeMillis())
38
+ put("severity", severity)
39
+ put("url", request.url.toString())
40
+ put("method", request.method)
41
+ put("requestBodySize", requestBodySize)
42
+ put("responseBodySize", responseBodySize)
43
+ put("durationMs", durationMs)
44
+ put("statusCode", response.code)
45
+ }
46
+
47
+ PerfAnalyzer.sendEvent(event)
48
+
49
+ return response.newBuilder()
50
+ .body(okhttp3.ResponseBody.create(responseBody?.contentType(), responseBytes))
51
+ .build()
52
+ }
53
+ }
@@ -0,0 +1,100 @@
1
+ package dev.perfanalyzer
2
+
3
+ import android.app.Application
4
+ import android.os.Handler
5
+ import android.os.Looper
6
+ import android.util.Log
7
+ import org.json.JSONObject
8
+ import java.net.URI
9
+ import javax.websocket.*
10
+
11
+ data class PerfConfig(
12
+ val groqKey: String? = null,
13
+ val geminiKey: String? = null,
14
+ val claudeKey: String? = null,
15
+ val port: Int = 7000,
16
+ val enableLocalAI: Boolean = true
17
+ )
18
+
19
+ object PerfAnalyzer {
20
+ private const val TAG = "PerfAnalyzer"
21
+ private var ws: Session? = null
22
+ private var config: PerfConfig = PerfConfig()
23
+ private var choreographerAdapter: ChoreographerAdapter? = null
24
+ private var memoryObserver: MemoryObserver? = null
25
+ private var logcatReader: LogcatReader? = null
26
+ private val mainHandler = Handler(Looper.getMainLooper())
27
+ private var running = false
28
+
29
+ fun start(context: Application, config: PerfConfig = PerfConfig()) {
30
+ if (running) return
31
+ running = true
32
+ PerfAnalyzer.config = config
33
+
34
+ connectWebSocket()
35
+
36
+ choreographerAdapter = ChoreographerAdapter { event -> sendEvent(event) }
37
+ mainHandler.post { choreographerAdapter!!.start() }
38
+
39
+ memoryObserver = MemoryObserver { event -> sendEvent(event) }
40
+ memoryObserver!!.start()
41
+
42
+ logcatReader = LogcatReader { event -> sendEvent(event) }
43
+ logcatReader!!.start()
44
+
45
+ Log.i(TAG, "🔍 Perf Analyzer connected")
46
+ Log.i(TAG, "📊 Dashboard → http://localhost:${config.port}")
47
+ }
48
+
49
+ fun stop() {
50
+ running = false
51
+ choreographerAdapter?.stop()
52
+ memoryObserver?.stop()
53
+ logcatReader?.stop()
54
+ ws?.close()
55
+ ws = null
56
+ }
57
+
58
+ internal fun sendEvent(event: JSONObject) {
59
+ try {
60
+ val session = ws
61
+ if (session != null && session.isOpen) {
62
+ session.basicRemote.sendText(event.toString())
63
+ }
64
+ } catch (e: Exception) {
65
+ Log.w(TAG, "Failed to send event: ${e.message}")
66
+ }
67
+ }
68
+
69
+ private fun connectWebSocket() {
70
+ Thread {
71
+ try {
72
+ val container = ContainerProvider.getWebSocketContainer()
73
+ val uri = URI("ws://localhost:7001")
74
+ ws = container.connectToServer(object : Endpoint() {
75
+ override fun onOpen(session: Session, config: EndpointConfig) {
76
+ Log.i(TAG, "WebSocket connected to bridge")
77
+ }
78
+
79
+ override fun onClose(session: Session, closeReason: CloseReason) {
80
+ Log.w(TAG, "WebSocket closed: $closeReason")
81
+ if (running) {
82
+ Thread.sleep(3000)
83
+ connectWebSocket()
84
+ }
85
+ }
86
+
87
+ override fun onError(session: Session, thr: Throwable) {
88
+ Log.e(TAG, "WebSocket error: ${thr.message}")
89
+ }
90
+ }, ClientEndpointConfig.Builder.create().build(), uri)
91
+ } catch (e: Exception) {
92
+ Log.e(TAG, "WebSocket connection failed: ${e.message}")
93
+ if (running) {
94
+ Thread.sleep(3000)
95
+ connectWebSocket()
96
+ }
97
+ }
98
+ }.start()
99
+ }
100
+ }
@@ -0,0 +1,73 @@
1
+ import 'dart:async';
2
+ import 'dart:convert';
3
+ import 'package:web_socket_channel/web_socket_channel.dart';
4
+ import 'src/universal_event.dart';
5
+ import 'src/vm_service_adapter.dart';
6
+ import 'src/frame_observer.dart';
7
+ import 'src/rebuild_observer.dart';
8
+ import 'src/network_observer.dart';
9
+
10
+ export 'src/universal_event.dart';
11
+ export 'src/network_observer.dart' show PerfHttpClient;
12
+
13
+ class PerfAnalyzer {
14
+ static VMServiceAdapter? _vmAdapter;
15
+ static FrameObserver? _frameObserver;
16
+ static RebuildObserver? _rebuildObserver;
17
+ static NetworkObserver? _networkObserver;
18
+ static WebSocketChannel? _channel;
19
+ static final List<StreamSubscription> _subs = [];
20
+
21
+ static Future<void> start({PerfConfig config = const PerfConfig()}) async {
22
+ final uri = Uri.parse('ws://127.0.0.1:${config.port}/ws');
23
+ _channel = WebSocketChannel.connect(uri);
24
+
25
+ void send(UniversalEvent event) {
26
+ try {
27
+ _channel?.sink.add(jsonEncode(event.toJson()));
28
+ } catch (_) {}
29
+ }
30
+
31
+ _vmAdapter = VMServiceAdapter();
32
+ try {
33
+ await _vmAdapter!.connect();
34
+ } catch (e) {
35
+ print('⚠️ VM Service unavailable: $e');
36
+ }
37
+
38
+ _frameObserver = FrameObserver(_vmAdapter!);
39
+ _rebuildObserver = RebuildObserver(_vmAdapter!);
40
+ _networkObserver = NetworkObserver();
41
+
42
+ _subs.add(_frameObserver!.events.listen(send));
43
+ _subs.add(_rebuildObserver!.events.listen(send));
44
+ _subs.add(_networkObserver!.events.listen(send));
45
+
46
+ // Also forward raw VM events for memory and error events
47
+ _subs.add(_vmAdapter!.eventStream
48
+ .where((e) => e.event != 'frame' && e.event != 'rebuild')
49
+ .listen(send));
50
+
51
+ // ignore: avoid_print
52
+ print('🔍 Perf Analyzer connected');
53
+ // ignore: avoid_print
54
+ print('📊 Dashboard → http://localhost:${config.port}');
55
+ }
56
+
57
+ static void stop() {
58
+ for (final sub in _subs) {
59
+ sub.cancel();
60
+ }
61
+ _subs.clear();
62
+ _frameObserver?.dispose();
63
+ _rebuildObserver?.dispose();
64
+ _networkObserver?.dispose();
65
+ _vmAdapter?.dispose();
66
+ _channel?.sink.close();
67
+ _frameObserver = null;
68
+ _rebuildObserver = null;
69
+ _networkObserver = null;
70
+ _vmAdapter = null;
71
+ _channel = null;
72
+ }
73
+ }