← Docs hub

리펙토링: Voice Agent 버퍼/F1-F2-F3/메모리 안정화 릴리즈

이 문서는 skmagic_ondeviceai_agent의 오디오 버퍼, F1/F2/F3 상태 전이, LLM/Cloud 호출 자원 정리, 메모리 retained buffer 대응, 실기기 배포와 검증 결과를 한 곳에 모은 릴리즈 기록이다.

요약

항목 결과
설치 경로 /system/priv-app/MaumAi/MaumAi.apk
패키지 com.skmagic.ondeviceai.agent
설치 버전 versionCode=124, versionName=1.19.36
서명 platform cert SHA-256 c8a2e9bccf597c2fb6dc66bee293fc13f2fc47ec77bc6b2b0d52c11f51192ab8
부팅 sys.boot_completed=1, app PID 유지
기기 테스트 connected Android test 6개 통과
F1/F2/F3 hard test realistic 300회, aggressive/mixed 200회, public receiver burst 100회 통과
direct service action start/reset/accept/stop 100회 통과
crash/tombstone/audio abort 확인된 signature 없음

변경 범위

파일 변경 내용 의도
app/src/main/java/com/skmagic/ondeviceai/agent/service/ForegroundService.kt llmDispatcher.close() 추가 LLM dispatcher thread pool 누수 방지
ForegroundService.kt GrowingFloatBuffer drain/clear 후 초기 capacity로 축소 긴 main recording 이후 큰 backing array를 계속 잡는 high-water retention 방지
ForegroundService.kt myService.dispose() 호출 수동 생성된 F-key 처리 객체의 timer/binding/listener 정리
app/src/main/java/com/skmagic/ondeviceai/agent/service/MyAccessibilityService.kt dispose() 추가, F2/F3/recordMinimum timer dispose, bound context unbind F1/F2/F3 timer scope와 service reference lifecycle 정리
app/src/main/java/com/skmagic/ondeviceai/agent/utils/AudioRecordManager.kt NonCancellable.isActive 대신 current coroutine context의 isActive 사용 오디오 read loop cancel 조건 명확화
app/src/main/java/com/skmagic/ondeviceai/agent/service/repository/ChatRepository.kt Cloud API call list lock 추가, finally remove, clear 시 snapshot cancel Cloud LLM 요청 추적 리스트 동시성/잔존 call 정리
app/src/main/cpp/main.cpp DsLog::UnInit()에서 GenieLog_free() 호출 log handle free 함수 오용 수정
app/src/main/java/com/skmagic/ondeviceai/agent/receiver/F123BroadcastReceiver.java API 33 이상/이하 getParcelableExtra 분기 F1/F2/F3 receiver 호환성 유지
app/src/androidTest/java/com/skmagic/ondeviceai/agent/ExampleInstrumentedTest.kt package assertion 보정 현재 앱 패키지명 기준 instrumentation test 통과
app/src/androidTest/java/com/skmagic/ondeviceai/agent/test/FunctionCallTest.kt <sk_4>(position=-1) 로컬 멀티턴 시작 instrumentation test 추가 첫 멀티턴 응답이 count를 올리고 대화 열린 상태를 유지하는지 회귀 방지
app/src/test/resources/*.json 현재 dictionary asset 기준 positive/negative fixture 보정 dictionary unit test가 실제 asset 결과를 검증하도록 정렬
app/src/test/java/com/skmagic/ondeviceai/agent/service/ForegroundServiceBufferTest.kt buffer 단위 테스트 추가 pre-F2 ring/growing buffer 동작 회귀 방지
scripts/repro_audio_read_race.sh meminfo before/during/after 수집 추가, CANCEL_AFTER_F2 옵션 추가 F1/F2/F3 hard test, 화면 취소 버튼에 대응되는 F2-cancel path, 메모리 추이 evidence 동시 확보

라이프사이클 보강 의미

Android Service lifecycle은 크게 started service와 bound service 흐름으로 나뉜다. Android 공식 문서의 Services overviewBound services 기준으로, started service는 startService() 또는 startForegroundService() 이후 onStartCommand()를 통해 시작되고 명시적으로 멈출 때까지 실행될 수 있다. bound service는 bindService()로 연결된 component가 있는 동안 client-server 인터페이스로 동작하고, 모든 client가 unbind되면 destroy 대상이 된다. 한 service가 started와 bound 흐름을 동시에 가질 수도 있다.

이 앱은 두 흐름이 섞여 있다. ForegroundService는 F1/F2/F3와 LLM/TTS action을 onStartCommand()로 받는 started foreground service이고, MyAccessibilityServiceForegroundService에 bind해서 F-key timer와 직접 method 호출 경로를 갖는다. 따라서 이번 lifecycle 보강의 핵심은 단순히 onDestroy()에 코드를 넣었다는 뜻이 아니라, started 경로와 bound 경로 양쪽의 retained reference를 끊는 것이다.

Voice Agent service lifecycle

정리하면 다음과 같다.

lifecycle 지점 기존 위험 보강 후 의미
ForegroundService.onStartCommand() F1/F2/F3 action 반복 중 buffer와 stop job이 이전 상태를 오래 잡을 수 있음 F2 reset과 stop/cancel 경로를 hard test로 반복 검증
ForegroundService.onDestroy() LLM dispatcher, coroutine scope, Cloud call, helper service reference가 남을 수 있음 dispatcher close, scope cancel, ChatRepository.clear(), myService.dispose()로 정리
MyAccessibilityService.bindService() 수동 생성 helper가 bind context를 잃으면 unbind 타이밍이 불명확해질 수 있음 boundContext를 저장해 dispose 시 정확히 unbind
F-key timers F2/F3/recordMinimum timer가 다음 입력 cycle까지 살아남을 수 있음 CancelableTimer.dispose()로 timer scope 자체를 종료
F2-cancel 사람이 화면의 취소 버튼을 눌러야만 cancel path 확인 가능 CANCEL_AFTER_F2=1 stress 옵션으로 STOP_RECORDING, IS_CANCEL=true, STATE_IDLE 경로 자동화

F2-cancel 자동화는 실제 화면 취소 버튼을 누르는 경로와 같은 service contract를 사용한다. F2 후 시스템이 main recording 상태로 반응할 시간을 주기 위해 비교 테스트에서는 CANCEL_AFTER_F2_DELAY_MS=1000을 사용했다.

설치 절차

  1. 기존 기기 APK 백업:
adb -s 192.168.123.108:5555 pull /system/priv-app/MaumAi/MaumAi.apk artifacts/device_backup/MaumAi_before_*.apk
  1. release APK 빌드:
./gradlew app:assembleRelease
  1. unsigned APK를 platform key로 수동 서명:
/home/silogood/Android/Sdk/build-tools/36.1.0/zipalign -p 4 \
  app/build/outputs/apk/release/app-release-unsigned.apk \
  app/build/outputs/apk/release/MaumAi-platform-aligned-20260722_012325.apk

/home/silogood/Android/Sdk/build-tools/36.1.0/apksigner sign \
  --ks platform.jks \
  --ks-key-alias platform \
  --out app/build/outputs/apk/release/MaumAi-platform-signed-20260722_012325.apk \
  app/build/outputs/apk/release/MaumAi-platform-aligned-20260722_012325.apk
  1. 서명 검증:
Signer #1 certificate SHA-256 digest:
c8a2e9bccf597c2fb6dc66bee293fc13f2fc47ec77bc6b2b0d52c11f51192ab8
  1. 기기 push:
adb -s 192.168.123.108:5555 root
adb -s 192.168.123.108:5555 remount
adb -s 192.168.123.108:5555 push \
  app/build/outputs/apk/release/MaumAi-platform-signed-20260722_012325.apk \
  /system/priv-app/MaumAi/MaumAi.apk
adb -s 192.168.123.108:5555 shell sync
adb -s 192.168.123.108:5555 reboot

별도 chmod, chown, restorecon은 수행하지 않았다.

기기 설치 검증

/system/priv-app/MaumAi/MaumAi.apk
package:/system/priv-app/MaumAi/MaumAi.apk
versionCode=124 minSdk=30 targetSdk=33
versionName=1.19.36
lastUpdateTime=2026-07-22 01:23:49
pid=6583
sys.boot_completed=1

기기에서 다시 pull한 /system/priv-app/MaumAi/MaumAi.apk도 platform cert SHA-256과 일치했다.

자동 테스트 결과

명령 결과
./gradlew app:testDebugUnitTest --rerun-tasks 성공
./gradlew app:testStageUnitTest --rerun-tasks 성공
./gradlew app:lintDebug 성공
./gradlew app:assembleDebug 성공
./gradlew app:externalNativeBuildStage 성공
./gradlew app:connectedDebugAndroidTest A1-13 기기에서 6개 테스트 성공
bash -n scripts/repro_audio_read_race.sh 성공

최종 재확인으로 ./gradlew app:testDebugUnitTest --rerun-tasks app:testStageUnitTest --rerun-tasks app:lintDebug app:assembleDebug도 성공했다.

Hard Test 결과

테스트 반복 PID 결과 evidence
realistic normal F1/F3 + random F2 300 6583 -> 6583 NO_CRASH_SIGNATURE artifacts/device_hardtest/20260722_013338
realistic aggressive + mixed F2 edge 200 6583 -> 6583 NO_CRASH_SIGNATURE artifacts/device_hardtest/20260722_015011
direct service action start/reset/accept/stop 100 6583 -> 6583 NO_CRASH_SIGNATURE artifacts/device_hardtest/20260722_020237
public F-key receiver burst 100 6583 -> 6583 NO_CRASH_SIGNATURE artifacts/device_hardtest/20260722_020541
text-input 2턴 처리 2 6583 -> 6583 PID_STABLE artifacts/device_hardtest/text_multiturn_20260722_021006

각 hard test는 다음 항목을 수집했다.

releaseBuffer, mUnreleased, Fatal signal, SIGABRT, Error processing audio data signature는 수집 로그에서 발견되지 않았다.

메모리 관찰

테스트 before PSS KB first sample last sample max sample after PSS KB 해석
realistic normal 300 342045 741655 935588 936152 934560 테스트 중 LLM/native model이 로딩되며 working set 상승
aggressive mixed 200 935556 934386 964717 965349 964265 edge 반복 중 crash 없이 완만한 working set 증가
service action 100 1133613 1106041 1111576 1113360 1108924 반복 후 after가 before보다 낮아짐
public F-key 100 1105455 1105439 1113792 1114020 1114823 receiver burst 후 약 9MB 증가, crash signature 없음

am send-trim-memory com.skmagic.ondeviceai.agent RUNNING_CRITICAL 이후에도 PID는 유지되었고 MEMORY_LOW action 로그가 확인되었다. Java heap은 trim 이후 낮아졌다.

192.168.10.5 기존/신규 APK 비교

2026-07-22에 새 보드 192.168.10.5:5555에서 기존 설치본과 신규 리팩토링 적용본을 같은 stress 계열로 비교했다.

구분 APK 설치 경로 버전 PID 비고
baseline 기존 설치본 /system/priv-app/MaumAi/MaumAi.apk 1.19.37 3308, reinit 후 3889 테스트 전 이미 설치되어 있던 APK
after 신규 platform-signed APK /system/priv-app/MaumAi/MaumAi.apk 1.19.36 3320 DB 삭제/재부팅 후 initialize 확인, lastUpdateTime=2026-07-22 09:51:51

Stress 결과 비교

테스트 baseline evidence baseline PSS KB after evidence after PSS KB 판정
realistic normal 300 artifacts/device_compare_192_168_10_5/baseline/20260722_091505 993566 -> 1041460 artifacts/device_compare_192_168_10_5/after/20260722_100138 848822 -> 828719 둘 다 PID 유지, after는 감소
aggressive mixed 200 baseline/20260722_093045 1034716 -> 1033425 after/20260722_101719 827149 -> 828286 둘 다 안정, after +1137 KB
direct service action 100 baseline/20260722_094230 1033385 -> 1035553 after/20260722_102918 828007 -> 833267 둘 다 안정, after +5260 KB
public F-key receiver burst 100 baseline/20260722_094426 1039929 -> 1037274 after/20260722_103125 828243 -> 831943 둘 다 안정, after +3700 KB
F2-cancel 80 baseline/20260722_094828 263531 -> 788616 after/20260722_103332 828484 -> 831024 baseline은 reinit 직후 첫 native load 포함, after warm 상태 +2540 KB
F2-cancel 80 warm repeat 없음 - after/20260722_103654 828638 -> 834191 cancel 반복 후 +5553 KB, 누적 폭증 없음

모든 baseline/after run은 before_pid == after_pid, result=NO_CRASH_SIGNATURE, tombstones_before.txttombstones_after.txt 0 line이었다. Fatal signal, SIGABRT, SIGSEGV, ANR, OutOfMemory, releaseBuffer, mUnreleased, Error processing audio data signature도 수집 로그에서 발견되지 않았다.

F2-cancel 메모리 해석

초기 baseline 20260722_094828263531 KB -> 788616 KB로 크게 증가했다. 이 값은 cancel path만의 누수로 보기 어렵다. 같은 run의 category diff에서 Java heap은 67492 KB -> 40624 KB로 줄었고, Native Heap PSS가 121872 KB -> 708324 KB로 증가했다. 즉 force-stop/reinit 후 첫 음성 경로 진입에서 native/STT/TTS/LLM working set이 로딩된 영향이 크다.

신규 APK에서는 이미 warm 상태에서 F2-cancel 80회를 두 번 반복했다.

로그에는 F2_CANCEL delay_ms=1000, ForegroundService - onStartCommand : action(STOP_RECORDING), stopRecording: recording canceled : true, Status set to STATE_IDLE가 확인되었다. 따라서 현재 evidence 기준으로는 F2-cancel 자체가 반복될수록 native heap을 계속 크게 누적시키는 패턴은 보이지 않는다.

멀티턴 영향 확인

F1/F2/F3 hard test와 direct service action은 기존 상태 전이 경로를 반복해서 통과했다.

자동화에서 확인된 것은 상태 전이/버퍼/프로세스 안정성, 그리고 로컬 function-call 멀티턴 시작 조건이다. FunctionCallTest#testSK4_localMultiturnStartKeepsConversationOpen<sk_4>(position=-1)<sk_end> 처리 후 LlmStore.multiTurnCount == 1이고 LlmStore.isResetMultiturn == false임을 A1-13 instrumentation에서 확인한다.

실제 대화형 멀티턴에서 multiTurnBusy=true가 되는지는 Cloud 응답이 질문형으로 돌아오고 사용자가 후속 발화를 넣는 실음성 시나리오가 필요하다. 이번 자동화에서는 text-input 2턴 처리까지는 PID 안정성을 확인했지만, 입력이 dictionary fallback으로 종료되어 multiTurnBusy=false 로그가 남았다.

따라서 멀티턴 회귀 확인은 다음 수동 실음성 절차를 추가 gate로 둔다.

  1. 하이나무 호출 후 질문형 응답이 나오는 발화를 수행한다.
  2. 응답이 끝난 뒤 F1 없이 후속 발화를 넣는다.
  3. 로그에서 multiTurnBusy=true, Updated cloud conversation state, 후속 MAIN_RECORDING 진입을 확인한다.
  4. 후속 발화 종료 후 resetState: multiTurnBusy=false로 정상 수렴하는지 확인한다.

결론

이번 리펙토링은 버퍼 소유권, retained memory, F1/F2/F3 timer lifecycle, Cloud API call 추적, native log handle 해제를 보강했다. 실기기 기준으로 system priv-app 배포, platform signing, 부팅, package load, instrumentation test 6개, F-key stress, direct service action, memory sampling까지 통과했다.

남은 리스크는 실제 음성 기반 Cloud 멀티턴의 의미 동작 검증이다. 상태 전이와 버퍼 안정성은 hard test에서 유지되었지만, 제품 수준의 멀티턴 응답 품질은 실음성 시나리오로 별도 확인해야 한다.

Keyboard shortcuts

⌘K / Ctrl+KOpen command palette
/Focus search
g hGo to home
g pGo to projects
g sGo to sessions
j / kNext / prev row (tables)
?Show this help
EscClose dialogs

Structured queries

Mix key:value filters with free text in the palette:

type:sessionOnly session pages
project:llm-wikiFilter by project name (substring)
model:claudeFilter by model name (substring)
date:>2026-03-01Sessions after a date
date:<2026-04-01Sessions before a date
tags:rustPages mentioning a tag/topic
sort:dateSort results by date (newest first)

Example: type:session project:llm-wiki date:>2026-04 sort:date