Design Projects Coding Projects Experiments / Arts Writing About Resume / CV ↗

Energy Analyzer

Can an LLM find the code that drains an Android battery?

A capstone project on Android energy anti-patterns: code that works but wastes battery, such as a wakelock that is never released. It pairs the DynAMICS research dataset with a full-stack web app that asks an LLM, per file and per pattern, whether the code has the problem, why it matters and how to fix it.

I built the app end to end and ran the evaluation that compares local and hosted LLMs against two research baselines.

Timeline
Feb – Apr 2026Capstone project
Role
Full-stack development, prompt design, evaluation
Stack
FastAPISQLAlchemyReactTypeScriptOllama / Gemini / OpenAIDockerRender

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.

Can a general-purpose LLM read Java source and flag energy anti-patterns well enough to sit between those two extremes, and explain the fix while it is at it?Research question

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).

CodePatternWhat it isSeverity
DWDurable WakelockWakeLock.acquire() without a matching release() on every code path.Critical
HASHeavy AsyncTaskBlocking I/O or CPU work inside onPreExecute, onPostExecute or onProgressUpdate.Major
IODInit OnDrawObject allocations such as new Paint() inside View.onDraw().Major
HMUHashMap UsageHashMap where ArrayMap or SparseArray would use less memory.Minor
NLMRNo Low-Memory ResolverAn 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.

01Keyword pre-filterA cheap regex pass. If a file has no WakeLock literal, DW is marked not applicable and never reaches the model.
02AST sliceOnly the scopes that matter are kept: onDraw bodies for IOD, AsyncTask callbacks for HAS, wakelock methods with their try/finally context for DW.
03One batched LLM callA single prompt covers every remaining pattern and returns structured JSON.
04Persist & surfaceVerdicts are stored per file and pattern. A failed pattern is flagged, it does not fail the whole file.

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
React 18
TypeScript
REST + 3 s polling
Backend
FastAPI
Async worker loop
AST slicer
Persistence
SQLite (default)
PostgreSQL (Docker)
LLM provider · pick one
Ollama, local
Google Gemini
OpenAI

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.

CancelSets a flag the worker checks between files, never mid-call.
RetryResets failed files to pending; the next poll picks them up.
DeployDocker Compose locally, or one-click to Render with managed Postgres.

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.

TimerApp · Upload · 4/4 files · 1,170 lines · 2h ago
D
Energy grade
density 6.0
Total issues44 files scanned
Critical1
Major1
Minor2

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.

Table view · files × patterns
FileDWHASIODHMUNLMR
TimerService.java YesNANANo Yes
ClockView.javaNANA YesNoNA
SyncTask.javaNANoNA YesNA
MainActivity.javaNoNANANoNo

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.

Code view · TimerService.java
10private void startTracking() {
11 PowerManager pm = (PowerManager) getSystemService(POWER_SERVICE);
12 wakeLock = pm.newWakeLock(PARTIAL_WAKE_LOCK, "timer");
13 wakeLock.acquire();
DWDurable WakelockCriticalline 12–14high

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.

Suggested changeCopy
13−wakeLock.acquire();
14−runTimer();
13+wakeLock.acquire();
14+try { runTimer(); }
15+finally { wakeLock.release(); }
14 runTimer();
15}

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.

Neutral first, colour secondThe page is cool white. Colour is reserved for semantic meaning: severity, success, error. It never decorates.
No coloured left-border accentsTried and rejected. Severity is signalled with a dot, a pill or text colour, never a vertical stripe.
One type systemInter for UI, Fraunces only for page-level headings and the grade letter, JetBrains Mono for actual code.
No nested cards, no invented spacingA card inside a card is a smell: use rows and dividers. Spacing comes from tokens only; 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.

Critical#DC2626 on #FEF2F2
Major#D97706 on #FFFBEB
Minor#CA8A04 on #FEFCE8
Success#16A34A on #F0FDF4
SeverityBadgeCriticalMajorMinorpilldot

Type scale

Display · 32Dashboard
H1 · 24Run history
H2 · 18Durable Wakelock
Body · 14The wake lock is acquired but never released.
Small · 13Upload · 1/1 file · 2h ago
Caption · 12Why it drains

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

4
8
12
16
20
24
32
40
48
64

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.

47
Java classes, 11 true positives
18
evaluation runs across 4 local models
7
prompt versions iterated
DetectorPrecisionRecallF1Accuracy
qwen2.5-coder:7b · prompt v4 / v70.380.550.440.68
qwen2.5-coder:7b · first prompt0.300.640.410.57
ChatGPT0.260.910.400.36
gemma3:12b · first prompt0.250.820.380.38
mistral · prompt v40.250.550.340.51
DynAMICS · dynamic baseline1.000.180.310.81
aDoctor · static baseline0.180.550.270.32

F1 score

qwen2.5-coder v4
44%
ChatGPT
40%
DynAMICS
31%
aDoctor
27%

Recall

ChatGPT
91%
qwen2.5-coder v4
55%
aDoctor
55%
DynAMICS
18%
A small local model can leadA 7B code model running on a laptop reached the best F1, ahead of ChatGPT and both research tools.
Prompts swing the resultThe same model went from an F1 of 0.44 to 0 between prompt versions. One variant flagged almost every file: recall 0.91, accuracy 0.26.
Nothing is production-readyDynAMICS never raises a false alarm but finds 2 of 11. The best LLM run still produced 10 false positives.
The LLM does not replace the research tools. It occupies the middle: better balanced than either baseline, and the only one that explains its answer.What the numbers say

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.