Cutting Android Camera-to-Preview Lag from 400ms to 44ms with Perfetto
Android camera-to-preview transitions are a common source of jank, and the visible symptoms — `sys_futex`, `sys_ioctl` — look like system problems. This walkthrough shows how to trace the real wait chain through Perfetto and proves that application-side decisions about image sizing, Surface lifecycle, and frame scheduling are the actual levers for fixing it.
After taking a photo, an Android app froze for hundreds of milliseconds. A Perfetto trace showed the main thread stuck in `sys_futex` waiting on the RenderThread, which itself was stuck in `sys_ioctl` waiting on the GPU and graphics driver. The root cause was not a system bug but application-level decisions: loading the full-resolution raw image directly into the UI, tearing down the camera Surface in the same frame the preview appeared, and packing multiple state updates into one frame.
Four rounds of targeted fixes brought the key frame duration from over 400ms down to roughly 44ms. The optimizations included downsampling the preview image to actual display size, keeping the camera preview alive briefly while the image overlays on top, spreading state updates across frames with `withFrameNanos`, and delaying CameraX unbind until the transition settles.
Perfetto SQL queries also revealed that after the first fix, the RenderThread was no longer sleeping but was `Runnable (Preempted)` — competing for CPU against CameraX threads, SurfaceFlinger, kernel memory compaction, and even the trace tooling itself. The investigation makes clear that Android UI jank often manifests as system calls, but the triggers are application-layer resource and timing choices.
The investigation reframes a common misinterpretation: sys_futex and sys_ioctl are not root causes but end-of-chain symptoms. The real question is what the thread is waiting on, and that answer lives in the parent call chain and the concurrent thread states.
Thread state (Sleeping vs. Runnable Preempted) is a more useful signal than the syscall name alone. The same sys_ioctl slice means entirely different things depending on whether the thread is blocked on a fence or starved for CPU.
Perfetto's UI timestamps cannot be used directly in SQL; the reliable path is to pull the real ts and dur from the slice table first, then query sched within that window. This is a practical gotcha for anyone writing Perfetto SQL.
The optimization is not about finding one bottleneck but about sequencing: the order and frame alignment of Surface removal, image decode, and state updates matter as much as the cost of each operation individually.
CameraX unbindAll in DisposableEffect.onDispose is a defensive pattern that prevents the camera stack from running after PreviewView leaves the tree — a subtle leak that can add sustained background load across multiple frames.