01.The Problem
Energy bugs do not crash an app, so tests rarely catch them. A screen that never sleeps or an object allocated on every frame only shows up as a hot phone and a bad review. Research tools exist, but they trade off badly: dynamic analysis is precise and misses most cases, static analysis catches more and floods the developer with false alarms.
The five patterns come from the TQRG taxonomy used by the DynAMICS paper (A Tool-Based Method for the Specification and Dynamic Detection of Android Behavioral Code Smells, TSE 2024).
| Code | Pattern | What it is | Severity |
|---|---|---|---|
| DW | Durable Wakelock | WakeLock.acquire() without a matching release() on every code path. | Critical |
| HAS | Heavy AsyncTask | Blocking I/O or CPU work inside onPreExecute, onPostExecute or onProgressUpdate. | Major |
| IOD | Init OnDraw | Object allocations such as new Paint() inside View.onDraw(). | Major |
| HMU | HashMap Usage | HashMap where ArrayMap or SparseArray would use less memory. | Minor |
| NLMR | No Low-Memory Resolver | An Activity or Service missing onLowMemory() / onTrimMemory(). | Minor |
02.Detection Pipeline
Sending whole files to a model is slow, expensive and noisy. Each Java file goes through a pipeline that narrows what the model sees before it is asked anything.
WakeLock literal, DW is marked not applicable and never reaches the model.onDraw bodies for IOD, AsyncTask callbacks for HAS, wakelock methods with their try/finally context for DW.For each pattern the model returns a verdict plus everything the interface needs to show a useful finding:
- Verdict
- Yes / No, with a reason and a confidence level.
- Diagnosis
- A short summary and a severity: minor, major or critical.
- Location
- Line range, an anchor line and a plain-language hint such as “inside startTracking() at line 12”.
- Fix
- The original snippet, a fixed snippet and an explanation of the change.
The slicer is regex and scope based, with no parser dependency, and falls back to the raw source when it cannot parse a file.
03.System Architecture
The system is deliberately small: one backend process and a single-page frontend. There is no separate worker, message broker or queue. Background analysis is an async loop inside the API process that polls for pending runs every two seconds and processes up to five files in parallel.
Frontend
Backend
Persistence
LLM provider · pick one
All three providers are called directly over HTTP rather than through their SDKs, so switching provider is a configuration change. Ollama keeps sensitive code on the machine; Gemini is the quick start; OpenAI powers the hosted demo.
04.Interface
The interface is modelled on a GitHub pull-request review, because that is where developers already read findings about their code. A run starts from uploaded .java files, a .zip, or a public GitHub URL. The components below are rebuilt here from the app’s own source and design tokens, with sample data.
Metric strip
The top of every run: an energy grade and a count per severity. Zero-count tiles stay in place, dimmed, so the layout is stable from run to run.
Recreated component with sample data.
Table view
A matrix of files × patterns. NA means the keyword pre-filter ruled the pattern out before any model call, which is different from the model answering No.
| File | DW | HAS | IOD | HMU | NLMR |
|---|---|---|---|---|---|
| TimerService.java | Yes | NA | NA | No | Yes |
| ClockView.java | NA | NA | Yes | No | NA |
| SyncTask.java | NA | No | NA | Yes | NA |
| MainActivity.java | No | NA | NA | No | No |
Recreated component with sample data.
Code view and inline finding card
The finding sits directly under the offending line. Issue lines get a subtle row tint and a coloured line number; the card carries the pattern tag, severity, line range and the model’s confidence, then the diagnosis, location hint and a suggested change as a mini diff.
The wake lock is acquired but never released if runTimer() throws, so the CPU stays awake until the process dies.
⌖ Inside startTracking() at line 12. Wrap the work in try / finally and release the lock there.
Recreated component. The Java snippet is an illustrative example, not a file from the evaluation set.
Energy grade
The grade is a weighted issue density per thousand lines of code, so a large clean codebase is not punished for one minor finding.
- Weights
- Critical × 3, Major × 2, Minor × 1
- Density
- Weighted score ÷ KLOC (falls back to file count when lines of code are unknown)
- Scale
- A: no issues · B: ≤ 2 · C: ≤ 4 · D: ≤ 7 · F: above 7
- Limitation
- Severity is static per pattern. Call-site context, such as a hot loop versus one-off init, is not weighted, and the app says so on its Rules page.
05.Design System
I wrote a design system for the app as durable rules, so that decisions made once did not have to be re-argued in every component.
14px is a bug.Severity colours
Three visibly different hues, because Major and Minor must not read as two shades of orange. Dots are one step punchier than text, since at 8px a text-level hue looks washed out.
Type scale
Weights stop at 600; heavier reads as shouting. Metadata is a single joined string in sans, never a mix of badges and mono.
Spacing tokens
Only these values are allowed. Component gaps use 8, 12 or 16; card padding 16, 20 or 24; section gaps 32 or 48. Cards have a 16px radius and no shadow: the product is flat.
06.Evaluation
I evaluated the Durable Wakelock pattern on 47 Java classes from the DynAMICS dataset, 11 of which are manually validated positives. Eighteen runs covered four local models across seven prompt versions, compared against ChatGPT and two research tools.
| Detector | Precision | Recall | F1 | Accuracy |
|---|---|---|---|---|
| qwen2.5-coder:7b · prompt v4 / v7 | 0.38 | 0.55 | 0.44 | 0.68 |
| qwen2.5-coder:7b · first prompt | 0.30 | 0.64 | 0.41 | 0.57 |
| ChatGPT | 0.26 | 0.91 | 0.40 | 0.36 |
| gemma3:12b · first prompt | 0.25 | 0.82 | 0.38 | 0.38 |
| mistral · prompt v4 | 0.25 | 0.55 | 0.34 | 0.51 |
| DynAMICS · dynamic baseline | 1.00 | 0.18 | 0.31 | 0.81 |
| aDoctor · static baseline | 0.18 | 0.55 | 0.27 | 0.32 |
F1 score
Recall
07.Reflection
The numbers are modest, and I think that is the honest result. Wakelock misuse depends on control flow across methods, which is exactly what a sliced prompt hides, so the next step is slicing that follows the acquire and release calls through the class rather than by method. The evaluation also covers one pattern on 47 files; the other four patterns have ground truth in the dataset and are wired into the scripts, but I have not run them at the same depth.
Building the product around the model mattered as much as the model. Structured output, per-pattern error handling and an interface that puts the finding under the line turned a yes/no classifier into something a developer could act on.