A draggable debug panel that shows how long each stage of a frame takes, and which one is slowing you down.
- A row per stage —
input,anim,layout,drawon the main thread, thensync,command,swapon the render thread, andgpu - Says why frames drop — thermal throttling, GC pauses, too few Choreographer ticks, or a late start
- Measures your app, not itself — the panel draws in its own window
- Fails tests on jank — a JUnit rule with thresholds
- Nothing in release builds —
debugImplementationleaves out the panel, its provider and theSYSTEM_ALERT_WINDOWit declares
dependencies {
debugImplementation("com.timkrest:framehud:0.5.0")
}There is nothing to call. A ContentProvider starts the panel, and it follows whichever activity
has focus.
Requires minSdk 24. Frame phases come from FrameMetrics; GPU timings need API 31+ and a driver
that reports them.
ui 118/s · 8.3ms 118 FPS
⚠ layout 8.4 ms
CPU now avg peak
input 0.1 0.2 1.1
anim 0.3 0.4 2.0
layout 7.9 8.4 22.3 ◀
draw 1.2 1.4 6.7
RENDER
sync 0.4 0.5 1.9
command 0.6 0.7 3.1
swap 0.2 0.3 1.4
GPU
gpu 2.1 2.4 9.8
delay 0.3 0.4 2.2
other 0.1 0.2
TOTAL 13.2 14.5 38.6
over 4.9 6.2 30.3
pipe:cpu 10.4 11.0
win jank 4.2% p95 12.1 max 22.3
ses p50 7.1 p95 12.4 p99 19.8
ses 4312f 1m12s jank 4.2% frz0 run3
mem 84/256 ▲96 · nat 37 ▲41 MB
gc x3 · 18 ms
therm none · hr 0.68
The header carries the main thread's Choreographer tick rate, the frame budget and FPS. The verdict
under it names the row to look at, and ◀ marks that row.
Columns read now avg peak: the current frame, the average over the window, and the peak since the
last reset. Rows summed from other rows stop after avg.
over is how far the frame ran past its budget. pipe: is the slowest stage. win is the window,
ses the session.
Reading the panel explains every row and what to do when one turns red.
debugImplementation already keeps everything out of a release build. Add framehud-noop only if
you call FrameHud outside src/debug — a release build still has to compile those lines:
releaseImplementation("com.timkrest:framehud-noop:0.5.0")It mirrors the API with empty bodies. The calls compile, nothing is measured, no window is added.
Settings live in one immutable config. Assign a copy and it takes effect at once.
FrameHud.config = FrameHud.config.copy(metricsSampleWindowFrames = 240)| Option | Default | What it controls |
|---|---|---|
enabled |
true |
While false, no window is added and no frames are collected. |
overlayMode |
PREFER_SYSTEM |
APP_WINDOW keeps the panel inside the app window and never uses the permission. |
eventListeners |
[LogcatEventListener] |
Who receives jank burst, frozen frame, thermal, screen and interaction events. |
metricsSampleWindowFrames |
120 |
How much history avg and the percentiles cover. |
metricsThrottleIntervalMs |
400 |
How often the panel may redraw. Lower values cost more to render. |
fallbackRefreshRateHz |
60 |
Refresh rate assumed when the display reports none. |
metricsThreadName |
framehud-metrics |
Name of the collecting thread, as it shows up in traces. |
show(), hide() and toggle() are shortcuts for enabled.
The panel is not the only way to read the numbers. FrameHud.metrics, memoryStats, thermalStats
and choreographerTicksPerSecond are plain StateFlows. A reading groups into phases (per-stage
timings), window (fps, jank and p95 over the sampling window), session (since the last reset) and
display (refresh rate and frame budget).
You get one event per jank burst, not one per frame, with the cause already worked out. The default listener writes to logcat.
FrameHud.config = FrameHud.config.copy(
eventListeners = listOf(
LogcatEventListener,
FrameHudEventListener { event -> analytics.log(event.summary) },
),
)Events arrive on the metrics thread. Don't block it and don't touch views from it.
A screen summary tells you which screen is slow. A mark tells you which interaction is.
FrameHud.mark = "scroll"
// the gesture runs
FrameHud.mark = nullFrames drawn while the mark is set belong to it. The header reads ▸ scroll instead of the timing
and every event fired meanwhile carries the name; clearing the mark reports a MarkEnded whose
stats cover that stretch alone. The panel's own rows keep covering the usual window and session —
the header labels them, it does not narrow them.
Leaving the screen clears the mark for you, so a gesture never spills into the next screen.
androidTestImplementation("com.timkrest:framehud-instrumentation:0.5.0")@get:Rule val noJank = DetectJankAfterTestSuccess(JankThresholds(maxJankPercent = 2f))The rule resets the collector before each test and checks the thresholds after the test passes, so a
failing test keeps its own error. Session totals outlive the panel, so there are still numbers after
ActivityScenario closes the activity.
To opt out, annotate a test or class with @SkipJankDetection, or call
JankAssertions.assertNoJank("scroll") at a point you choose.
With SYSTEM_ALERT_WINDOW granted, the panel lives in a system window and survives moving between
screens. Without it the window belongs to the current activity, so the panel is recreated on every
screen change, and a ⧉ button appears that opens the permission screen.
The library never opens it on its own. Set overlayMode = APP_WINDOW to stay in the app window and
hide that button.
On an emulator
The render thread and the GPU belong to the host machine, so those rows describe your desktop, not a
device. The panel marks the header EMU, labels those sections · host and greys them out.
Main-thread phases, jank and the session totals stay meaningful. That is what a jank gate on CI reads.
Installing by hand instead of the provider
Drop the provider and call FrameHud.install(application) from Application.onCreate(). The panel
comes up with the next resumed activity, so a call made later skips the screen already open.
<provider
android:name="com.timkrest.framehud.FrameHudInstaller"
android:authorities="${applicationId}.framehud-installer"
tools:node="remove" />./gradlew :sample:installDebug
A 300-row list with five toggles: blocking the main thread, overdrawing, allocating per row, nesting layouts, churning garbage. Each one moves a different metric.
- Reading the panel — what every row means, how to measure a screen, and what to do when something turns red
- API reference — generated from the sources of each release
- Roadmap — what is planned next, and what is deliberately not
- Changelog — what changed in each release
- Contributing — how to build, and what to check before opening a pull request. Contributions are covered by a CLA, which a bot will ask you to sign.
