Android 15(API 수준 35) 이상에서는 앱 기반 프로파일링을 통해 앱이 ProfilingManager API 또는 AndroidX Core(androidx.core:core 1.15.0 이상) 빌더를 사용하여 코드에서 지원되는 프로필 기록을 명시적으로 시작하고 중지할 수 있습니다. 프로필 타이밍 또는 세션 매개변수(예: 기간, 버퍼 정책, 샘플링 레이트)를 제어하거나 특정 사용자 여정 또는 런타임 조건을 타겟팅하려면 직접 프로파일링을 사용하세요.
종속 항목 추가
ProfilingManager API를 최적의 환경에서 사용하려면 다음 Jetpack 라이브러리를 build.gradle.kts 파일에 추가하세요.
Kotlin
dependencies { implementation("androidx.tracing:tracing-ktx:2.0.5") implementation("androidx.core:core:1.19.1") }
Groovy
dependencies { implementation 'androidx.tracing:tracing-ktx:2.0.5' implementation 'androidx.core:core:1.19.1' }
시스템 트레이스 기록
필수 종속 항목을 추가한 후 다음 코드를 사용하여 시스템 트레이스를 기록합니다. 이 예시에서는 기본 스레드 외부에서 무거운 작업을 안전하게 관리하면서 프로파일링 세션을 시작하는 방법을 보여줍니다.
Kotlin
@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM) @Composable fun ProfiledScreen(modifier: Modifier = Modifier) { // Use the application context: requestProfiling resolves the ProfilingManager // system service from it, so there's no reason to hand it a short-lived Activity. val appContext = LocalContext.current.applicationContext val scope = rememberCoroutineScope() Button( onClick = { // Run the orchestration off the main thread. Profiling a heavy operation // on the UI thread would freeze the UI (ANR) and distort the very metrics // you're trying to capture. // // Note: this scope is tied to composition. If the user leaves this screen // mid-session, the coroutine is cancelled and stopSignal.cancel() might not // run, but setDurationMs() acts as a safety net and ends the profile. scope.launch(Dispatchers.Default) { val callbackExecutor = Dispatchers.IO.asExecutor() val resultCallback = Consumer<ProfilingResult> { profilingResult -> if (profilingResult.errorCode == ProfilingResult.ERROR_NONE) { Log.d("ProfileTest", "Result file: ${profilingResult.resultFilePath}") } else { // errorMessage explains the failure (e.g., rate limiting); keep it. Log.e( "ProfileTest", "Profiling failed errorCode=${profilingResult.errorCode} " + "errorMessage=${profilingResult.errorMessage}" ) } } val stopSignal = CancellationSignal() val requestBuilder = SystemTraceRequestBuilder().apply { setCancellationSignal(stopSignal) setTag("FOO") // Caller-supplied tag for identification. setDurationMs(60000) // Hard cap: ends the session if cancel() never fires. setBufferFillPolicy(BufferFillPolicy.RING_BUFFER) setBufferSizeKb(32768) } // 1. Start the session. This is asynchronous system IPC. The tracing // engine takes a moment to start and allocate buffers. requestProfiling(appContext, requestBuilder.build(), callbackExecutor, resultCallback) // 2. The API exposes no "profiling started" signal, so pad with a short, // best-effort delay before running the code you care about. This is // approximate. Increase it on slower or heavily loaded devices. delay(STARTUP_PADDING_MS) // 3. The session is already recording every thread in your app. This slice // doesn't scope what's captured. It just labels this region of the // timeline so heavyOperation() is easier to find. trace { } closes the // section even if the block throws. trace("MyApp:HeavyOperation") { heavyOperation() } // 4. Stop recording. Until this fires or the setDurationMs() cap is // reached (whichever comes first), the session keeps capturing app-wide // activity. stopSignal.cancel() } } ) { Text("Run & Profile Heavy Operation") } } // Best-effort wait for the system trace engine to initialize before profiling. // There is no deterministic start callback; tune this for your target devices. private const val STARTUP_PADDING_MS = 100L fun heavyOperation() { // Background computations to profile. }
자바
void sampleRecordSystemTrace(Context context) { // Use the application context: requestProfiling resolves the ProfilingManager // system service from it, so there's no reason to hand it a short-lived Activity. Context appContext = context.getApplicationContext(); // Run the orchestration off the main thread. Profiling a heavy operation // on the UI thread would freeze the UI (ANR) and distort the very metrics // you're trying to capture. profilingExecutor.execute(() -> { Executor callbackExecutor = Executors.newSingleThreadExecutor(); Consumer<ProfilingResult> resultCallback = profilingResult -> { if (profilingResult.getErrorCode() == ProfilingResult.ERROR_NONE) { Log.d("ProfileTest", "Result file: " + profilingResult.getResultFilePath()); } else { // errorMessage explains the failure (e.g., rate limiting); keep it. Log.e( "ProfileTest", "Profiling failed errorCode=" + profilingResult.getErrorCode() + " errorMessage=" + profilingResult.getErrorMessage()); } }; CancellationSignal stopSignal = new CancellationSignal(); SystemTraceRequestBuilder requestBuilder = new SystemTraceRequestBuilder(); requestBuilder.setCancellationSignal(stopSignal); requestBuilder.setTag("FOO"); // Caller-supplied tag for identification. requestBuilder.setDurationMs(60000); // Hard cap: ends the session if cancel() never fires. requestBuilder.setBufferFillPolicy(BufferFillPolicy.RING_BUFFER); requestBuilder.setBufferSizeKb(32768); // 1. Start the session. This is asynchronous system IPC. The tracing // engine takes a moment to start and allocate buffers. Profiling.requestProfiling( appContext, requestBuilder.build(), callbackExecutor, resultCallback); // 2. The API exposes no "profiling started" signal, so pad with a short, // best-effort delay before running the code you care about. This is // approximate. Increase it on slower or heavily loaded devices. try { Thread.sleep(STARTUP_PADDING_MS); } catch (InterruptedException e) { Thread.currentThread().interrupt(); stopSignal.cancel(); return; } // 3. The session is already recording every thread in your app. This slice // doesn't scope what's captured. It just labels this region of the // timeline so heavyOperation() is easier to find. try/finally closes the // section even if heavyOperation() throws. Trace.beginSection("MyApp:HeavyOperation"); try { heavyOperation(); } finally { Trace.endSection(); } // 4. Stop recording. Until this fires or the setDurationMs() cap is // reached (whichever comes first), the session keeps capturing app-wide // activity. stopSignal.cancel(); }); } // Best-effort wait for the system trace engine to initialize before profiling. // There is no deterministic start callback; tune this for your target devices. private static final long STARTUP_PADDING_MS = 100L; // Single background thread that runs the profiling orchestration. private final ExecutorService profilingExecutor = Executors.newSingleThreadExecutor(); void heavyOperation() { // Background computations to profile. }
샘플 코드는 다음 단계에 따라 프로파일링 세션을 설정하고 관리합니다.
- 실행기 설정: 기본 스레드 외부에서 프로파일링 결과를 수신하는 UI가 아닌
Executor를 만듭니다. - 프로파일링 결과 처리:
Consumer<ProfilingResult>콜백을 만들어ProfilingManager에서 완료된 결과 또는 오류를 수신합니다. 프로파일링 요청 빌드:
SystemTraceRequestBuilder를 만들어 세션 설정을 맞춤설정합니다(또는 맞춤설정을 생략하여 시스템 기본값 사용).setTag():ProfilingResult.getTag에서 반환되고 출력 파일 이름에 추가되는 호출자 제공 태그를 연결합니다(처음 20개의 소문자 영숫자 문자 및 하이픈까지).setDurationMs(): 최대 녹화 시간을 밀리초 단위로 설정합니다(예: 60초의 경우60000).setBufferFillPolicy(): 버퍼가 용량 제한을 처리하는 방식을 정의합니다.BufferFillPolicy.RING_BUFFER는 버퍼가 가득 차면 가장 오래된 데이터를 덮어쓰고BufferFillPolicy.DISCARD는 버퍼가 가득 차면 새 데이터 기록을 중지합니다.setBufferSizeKb(): 녹음 버퍼 크기를 킬로바이트 단위로 설정합니다.
선택사항: 녹화 조기 중지:
CancellationSignal(stopSignal)를 전달하여 대상 작업이 완료되는 즉시 세션을 중지하고 지금까지 녹화된 데이터를 저장하고ProfilingResult를 콜백에 전달합니다. 자세한 내용은 활성 녹음 중지를 참고하세요.시작 및 결과 수신:
requestProfiling()를 호출하여 백그라운드에서 녹화를 시작합니다. 세션이 종료되면resultCallback.accept()는ProfilingResult를 수신합니다.- 성공(
ERROR_NONE) 시getResultFilePath()를 호출하여 기기 내 경로를 가져옵니다.adb로 파일을 가져오거나WorkManager로 업로드하는 방법은 프로필 검색 및 업로드를 참조하세요. - 실패 시
getErrorCode()및getErrorMessage()를 검사합니다. 비율 제한 및 기타 여러 가지 이유로 요청이 실패할 수 있습니다. - 결과가 전송되기 전에 앱이 종료되면 시스템은 결과를 큐에 추가하고 다음에 앱이
registerForAllProfilingResults를 사용하여 전역 리스너를 등록할 때 결과를 전송합니다.
- 성공(
커스텀 트레이스 포인트 추가:
trace("...") { ... }(또는Trace.beginSection()/Trace.endSection())에서 작업을 래핑하여 타임라인의 영역에 라벨을 지정합니다.
활성 상태의 녹화 중지하기
setDurationMs()가 만료될 때까지 기다리지 않고 상호작용이 완료되는 즉시 캡처를 중지하려면 CancellationSignal를 setCancellationSignal() 또는 requestProfiling()에 전달합니다. CancellationSignal와 setDurationMs()를 모두 지정하면 먼저 발생하는 항목이 세션을 종료합니다. 둘 다 지정하지 않으면 시스템에서 기본 기간을 적용합니다.
cancel()를 호출해도 녹음이 삭제되지 않습니다. 시스템은 캡처를 중지하고, 해당 시점까지 수집된 데이터를 수정 및 저장하고, ERROR_NONE를 사용하여 ProfilingResult를 리스너에 전달합니다.
호출 스택 샘플링으로 시스템 트레이스 기록
Android 17(API 수준 37) 이상(또는 업데이트된 Profiling 메인라인 모듈이 있는 Android 16)을 실행하는 기기에서 ProfilingManager는 단일 세션에서 시스템 트레이스와 함께 샘플링된 호출 스택을 수집하는 기능을 지원하므로 명시적 트레이스 계측이 없는 메서드의 CPU 호출 스택과 함께 스레드 상태, 프레임, 맞춤 트레이스 슬라이스와 같은 타임라인 이벤트를 확인할 수 있습니다.
이 예시에서는 지원되는 프로필 유형 중 하나(PROFILING_TYPE_SYSTEM_TRACE)를 지정하고 구성 Bundle을 전달하여 Jetpack 빌더 없이 ProfilingManager.requestProfiling를 직접 호출하는 방법도 보여줍니다.
Kotlin
val profilingManager = context.getSystemService(ProfilingManager::class.java) // Pass a Bundle to customize the capture, or pass null for default settings. val config = Bundle().apply { putInt("KEY_DURATION_MS", 15000) // 15 seconds putInt("KEY_SIZE_KB", 20480) // 20 MB buffer size // Collect sampled CPU call stacks alongside the system trace. putBoolean("KEY_COLLECT_STACK_SAMPLING", true) putInt("KEY_FREQUENCY_HZ", 100) // Sampling frequency in Hz // Optional: Set KEY_SAMPLE_BINDER_ONLY to true to sample only binder calls. } profilingManager?.requestProfiling( ProfilingManager.PROFILING_TYPE_SYSTEM_TRACE, config, "trace-with-callstacks", null, callbackExecutor, resultCallback )
자바
ProfilingManager profilingManager = context.getSystemService(ProfilingManager.class); if (profilingManager == null) return; // Pass a Bundle to customize the capture, or pass null for default settings. Bundle config = new Bundle(); config.putInt("KEY_DURATION_MS", 15000); // 15 seconds config.putInt("KEY_SIZE_KB", 20480); // 20 MB buffer size // Collect sampled CPU call stacks alongside the system trace. config.putBoolean("KEY_COLLECT_STACK_SAMPLING", true); config.putInt("KEY_FREQUENCY_HZ", 100); // Sampling frequency in Hz // Optional: Set KEY_SAMPLE_BINDER_ONLY to true to sample only binder calls. profilingManager.requestProfiling( ProfilingManager.PROFILING_TYPE_SYSTEM_TRACE, config, "trace-with-callstacks", null, callbackExecutor, resultCallback );
Perfetto UI에서 결과 프로필을 열면 샘플링된 호출 스택이 앱의 일반 트레이스 슬라이스와 함께 전용 호출 스택 트랙으로 표시됩니다. 그림 1과 같이 호출 스택 트랙의 영역을 선택하여 해당 기간의 샘플링된 호출 스택의 Flame 그래프를 확인합니다.

Java 힙 덤프 캡처
JavaHeapDumpRequestBuilder(또는 PROFILING_TYPE_JAVA_HEAP_DUMP)를 사용하여 앱의 자바 메모리 스냅샷을 요청할 수 있습니다. Debug.dumpHprofData로 캡처된 표준 .hprof 힙 덤프와 달리 ProfilingManager 힙 덤프는 프로덕션에서 안전하고 오버헤드가 적은 수집을 위해 설계되었습니다.
- 정지되지 않는 수집: 시스템은 앱 프로세스의 copy-on-write 클론을 포크하고 클론된 프로세스에서 힙 스냅샷을 수집하므로 덤프가 디스크에 기록되는 동안 앱이 정지되지 않습니다.
- 참조 그래프만(원시 객체 데이터 없음):
ProfilingManager는.hprof파일과 같은 원시 메모리 콘텐츠를 덤프하는 대신 클래스, 객체 크기, 참조 체인, 가비지 컬렉션(GC) 루트가 포함된.perfetto-java-heap-dumpprotobuf 그래프를 기록하고 기본 값, 문자열 콘텐츠, 배열 페이로드는 생략합니다. 이렇게 하면 사용자 개인 정보가 보호되고 파일 크기가 줄어듭니다. 자세한 내용은 ProfilingManager 힙 덤프와 HPROF 덤프의 차이점을 참고하세요.
시스템 트레이스 기록에 표시된 것과 동일한 Context, Executor, resultCallback 설정을 사용하여 JavaHeapDumpRequestBuilder로 요청을 빌드합니다.
Kotlin
val heapDumpRequest = JavaHeapDumpRequestBuilder() .setTag("memory-leak-check") .build() requestProfiling( appContext, heapDumpRequest, callbackExecutor, resultCallback )
자바
ProfilingRequest heapDumpRequest = new JavaHeapDumpRequestBuilder() .setTag("memory-leak-check") .build(); Profiling.requestProfiling( getApplicationContext(), heapDumpRequest, callbackExecutor, resultCallback );
Java 힙 덤프를 분석하여 메모리 누수를 진단하는 전체 엔드 투 엔드 둘러보기는 우수사례: OOM 비정상 종료 디버그를 참고하세요.
다음 단계
프로필을 캡처한 후 adb를 사용하여 로컬에서 검색하거나 WorkManager를 사용하여 백엔드에 업로드합니다.