Apache-2.0 · debug builds only

A window into your running app

Porthole is a debug-only window into a running Android app — for the developer watching it, and the coding agent fixing it.

QUICK START

The plugin, and one line per client

The runtime starts with your process and finds the Activity itself, so there is nothing to wrap. What it cannot discover is your HTTP and database clients: instrumenting one means being handed the builder before it is built.

01 Apply the plugin
plugins {
  id("live.gravitylabs.porthole")
}

It puts the runtime on your debug build types and a no-op with the same API on everything else.

02 Instrument your clients
import live.gravitylabs.porthole.*

OkHttpClient.Builder().installPorthole()
Room.databaseBuilder(…).installPorthole()

Ktor and SQLDelight too. Forget one and the lane says so, rather than looking like an app that made no requests.

03 Open the window
./gradlew portholeUi

Forwards the port, serves the timeline, opens your browser. No restart, no launch flag.

WHAT IT CAPTURES

Eleven lanes on one clock

Every lane shares a single monotonic clock, so a recomposition burst, the state writes that preceded it, and the frames it cost line up in the same column.

recompositions
Per-composable counts and peak per frame, down to the call site.
state writes
Yours separated from framework writes, named from the owner that holds them.
dropped frames
Counted in refreshes, so a 400ms freeze is not one missed frame.
main thread
Stalls over 100ms with the stack, and queries that ran on the main thread.
navigation
Destinations with their arguments and the deep link that got you there.
http
OkHttp or Ktor, with phases, status and bodies when you ask for them.
db
Room or SQLDelight: the SQL, the bound values, and the thread it ran on.
work
One bar per WorkManager attempt, so a retry is visible rather than averaged away.
memory
Java heap against total RAM, allocation rate, and collections that paused the app.
device
Rotation, foreground and background, trim memory, battery, doze, network.
warnings
Your own logcat at warning and above, stack traces intact.
FOR THE AGENT

The same session, in a form an agent can read

A screen recording tells a coding agent nothing. Porthole ships an MCP server that answers against the window you are looking at — recomposition counts, the state keys written just before them, what held the main thread and the stack it was stuck in.

  • Press ask agent and it copies the window as bounds, not prose, so the agent asks about the moment you actually saw.
  • Every finding says how strongly it can be claimed — observed when the device said so, correlated when two things merely happened near each other.
  • Local only. The socket binds to loopback and nothing leaves the machine.
ASK AGENT
Why does the cart screen stutter when I add an item?
CartViewModel.tick preceded 445 of 490 recompositions in this window, most often in Cart.ItemRow. The main thread was also blocked for 305ms in CartViewModel.blockTheMainThread.
observed · ordering, not proof of cause
ONE WINDOW FROM THE SAMPLE

What eight seconds looked like

The sample deliberately misbehaves: a ticking value is read inside every list row, and one button blocks the main thread. This is what the window said about it.

RECOMPOSITION cart screen
recompositions
490
preceded by tick
445
hottest node
Cart.ItemRow
state key
CartViewModel.tick
FRAMES AND MAIN THREAD same window
frames missed
158
worst frame
276ms
spent in
swapBuffers
main thread blocked
305ms

one capture from the sample app — your own numbers will differ

FAQ

The usual objections

Does it end up in my release APK?
No. The plugin puts the runtime on debug build types and a no-op artifact with the same public API on everything else. An API parity test fails the build if an integration ever ships without its no-op.
What does it cost at runtime?
Per instrumented recomposition: a timestamp, a short backwards walk over the last ~32ms of state writes, an object into a ring, and a queue offer. Event fan-out happens on a writer thread, never on the thread that emitted.
Does the device need to be rooted?
No. Everything goes over a standard adb port forward, and the socket binds to loopback only.
Views, or only Compose?
Frames, main thread, navigation, http, db, work, memory, device and logcat work in any app. Recompositions and state writes are Compose-specific.
Will it leak a token into a trace?
Query-string values and sensitive headers are replaced before anything leaves the process, and bodies are not captured unless you ask. The sample sends three secrets containing do-not-log so you can grep a capture and check.
ROADMAP

Next up

  • Publish to Maven Central and the Plugin Portal next
  • Compose Multiplatform core exploring
  • Control surface for out-of-process drivers exploring
  • Verified on a physical device planned
  • Stack symbolication for minified builds planned