A debuggable app's runtime speaks the Java Debug Wire Protocol (JDWP). Studio's debugger forwards a local port to it; so can you, and the JDK's command-line jdb then sets breakpoints as the Studio gutter does. am start -D holds the app at launch until a debugger attaches:
adb shell am start -D -n com.example.booknest/.MainActivity
adb forward tcp:8716 jdwp:$(adb shell pidof -s com.example.booknest)
jdb -attach localhost:8716
> stop in com.example.booknest.ui.CatalogViewModel.refresh
> contBreakpoint hit: "thread=main", com.example.booknest.ui.CatalogViewModel.refresh(), line=51 bci=0 main[1] where [1] com.example.booknest.ui.CatalogViewModel.refresh (ViewModels.kt:51) [2] com.example.booknest.ui.CatalogViewModel.<init> (ViewModels.kt:47) ... [14] com.example.booknest.ui.CatalogRouteKt.CatalogRoute (ViewModel.kt:65) ... [168] com.android.internal.os.ZygoteInit.main (ZygoteInit.java:932) main[1] print this.load.getValue() this.load.getValue() = "Loading"
Read bottom-up, the 168 frames run from the zygote through a Choreographer frame, Compose 234 measuring the Scaffold and hiltViewModel() in CatalogRoute to Hilt's factory and the constructor. Studio's Debug window shows the same frames over the same protocol.
Layout Inspector shows the running Compose hierarchy with each node's parameters and semantics, and, for API 29+ and Compose 1.2+, how often each composable recomposed and was skipped. Recomposition Counts reproduces those counts in logcat.