모바일 디바이스 팜 배터리 소모율·GPU 발열 임계치 초과 자동 탐지 구현 가이드
모바일 디바이스 팜에서 배터리 소모율과 GPU 발열 임계치 초과를 자동 탐지하는 방법을 정리합니다. Android Thermal API, 전류·온도 샘플링, 기기별 기준선 비교, CI 실패 판정과 알림까지 실전 운영 절차를 다룹니다.

핵심 요약
모바일 디바이스 팜의 배터리 소모율은 동일한 테스트 시나리오를 일정 시간 실행한 뒤 시작·종료 잔량과 전류 또는 충전량 데이터를 비교해 계산한다. GPU 발열은 기기에서 제공하는 온도 센서와 Android Thermal API를 함께 샘플링하고 임계치 초과가 일정 시간 지속될 때 자동 실패로 판정하면 된다. 핵심은 단일 온도값이 아니라 배터리·GPU 부하·프레임 시간·열 상태를 동일한 테스트 실행 ID로 묶어 분석하는 것이다.
TL;DR
- 배터리 퍼센트만 사용하지 말고
charge_counter_uah또는 평균 전류를 함께 기록한다. - GPU 온도는 기기별 센서 제공 여부가 다르므로
gpu_temp_c가 없을 때는thermal_status, GPU 사용률, 클럭, 프레임 시간을 보조 지표로 사용한다. - 절대 임계치와 기기별 기준선 대비 회귀율을 함께 적용한다.
- 1회 샘플 초과가 아니라 10~30초 이상 지속된 초과만 CI 실패로 처리해 오탐을 줄인다.
왜 모바일 디바이스 팜에서 배터리와 GPU 열을 함께 봐야 하는가?
배터리 소모와 GPU 발열은 같은 성능 문제의 서로 다른 결과일 수 있다. 렌더링 부하가 높으면 GPU 전력 사용량이 증가하고 온도가 상승한다. 이후 열 스로틀링이 시작되면 프레임 시간이 길어지고 같은 장면을 처리하는 데 더 많은 시간이 걸려 배터리 소모가 다시 커질 수 있다.
배터리 잔량만 비교하면 충전 상태나 배터리 게이지 보정의 영향을 받는다. 따라서 최소한 다음 지표를 함께 수집해야 한다.
| 지표 | 단위 | 권장 수집 위치 | 용도 |
|---|---|---|---|
battery_level_pct | % | Android BatteryManager | 빠른 추세 확인 |
charge_counter_uah | µAh | BatteryManager.BATTERY_PROPERTY_CHARGE_COUNTER | 실제 소모량 근사 |
battery_current_ua | µA | BatteryManager.BATTERY_PROPERTY_CURRENT_NOW 또는 벤더 센서 | 순간 전류와 스파이크 확인 |
gpu_temp_c | °C | 벤더 센서 또는 디바이스 에이전트 | GPU 온도 임계치 판정 |
thermal_status | 0~6 | Android PowerManager | 시스템 열 상태와 스로틀링 확인 |
gpu_busy_pct | % | 벤더 GPU 카운터 또는 Perfetto | 발열 원인 분석 |
frame_time_p95_ms | ms | 프레임 계측 | 열 스로틀링에 따른 체감 성능 확인 |
배터리 퍼센트 기준 소모율은 다음처럼 계산한다.
여기서 R_battery는 시간당 배터리 소모율(%/h), B_start와 B_end는 시작·종료 배터리 퍼센트, t_min은 테스트 시간(분)이다.
충전량 카운터를 사용할 수 있다면 다음 계산이 더 유용하다.
Q는 µAh 단위의 충전량이고 R_Q는 mAh/h다. 일부 기기는 전류의 부호가 반대로 표시될 수 있으므로 디바이스 등록 단계에서 방전 방향을 확인하고 모든 이벤트를 방전 양수 규칙으로 정규화해야 한다.
GPU 발열 임계치 초과는 왜 발생하는가?
GPU 온도 상승은 단순히 GPU 사용률이 높아서만 발생하지 않는다. 다음 요인이 함께 작용한다.
- 높은 셰이더 복잡도와 오버드로우
- 고해상도 렌더 타깃 또는 MSAA 사용
- 60fps 이상을 유지하기 위한 지속적인 GPU 클럭 상승
- 화면 밝기와 무선 통신으로 인한 기기 전체 전력 증가
- 케이스, 주변 온도, 초기 배터리 온도 차이
- 열 스로틀링 이후 동일 작업을 더 긴 시간 동안 수행하는 상황
Android 10(API 29) 이상에서는 PowerManager.currentThermalStatus로 시스템 열 상태를 읽을 수 있다. 일반적으로 THERMAL_STATUS_SEVERE 이상인 값인 3 이상은 성능 저하가 발생할 가능성이 큰 상태로 취급할 수 있다. 단, 제조사별 센서 구성과 냉각 설계가 다르므로 85°C 같은 절대값은 시작점일 뿐 모든 기기의 공통 합격 기준은 아니다.
배터리 소모율과 GPU 발열을 어떻게 수집할까?
1단계: 재현 가능한 테스트 조건을 고정한다
자동 탐지의 정확도는 측정 코드보다 테스트 조건에 더 크게 좌우된다. 다음 조건을 테스트 프로토콜에 포함한다.
- 동일한 앱 빌드와 테스트 데이터 사용
- 화면 밝기, 주사율, 음량, 네트워크 상태 고정
- 충전 중 여부를 명시하고 배터리 소모 테스트에서는 가능하면 충전을 차단
- 초기 배터리 잔량과 초기 기기 온도 범위 제한
- 워밍업 구간을 두고 측정 구간만 결과에 반영
- 최소 3회 반복 후 평균보다 중앙값과 분산을 함께 저장
충전 케이블을 연결한 상태에서는 외부 전원이 배터리 방전을 가릴 수 있다. USB 데이터 전용 연결이나 전원 제어가 가능한 장비를 사용하고 is_charging 상태를 반드시 결과에 기록해야 한다.
2단계: Android 에이전트에서 공통 이벤트를 만든다
앱 내부 계측과 디바이스 팜 실행기는 같은 스키마를 사용해야 한다. GPU 온도 센서가 없는 기기에서는 해당 값을 null로 저장하고 열 상태와 보조 지표로 판정한다.
{
"run_id": "run-20260824-001",
"device_model": "example-device",
"os_build": "example-build",
"ts_ms": 1724457600000,
"battery_level_pct": 82,
"charge_counter_uah": 3124000,
"battery_current_ua": -640000,
"is_charging": false,
"gpu_temp_c": 78.4,
"thermal_status": 2,
"gpu_busy_pct": 91,
"frame_time_p95_ms": 18.2
}
Android 앱에서 열 상태와 배터리 기본값을 수집하는 예시는 다음과 같다.
class DeviceHealthSampler(private val context: Context) {
private val powerManager = context.getSystemService(Context.POWER_SERVICE) as PowerManager
private val batteryManager = context.getSystemService(Context.BATTERY_SERVICE) as BatteryManager
fun sample(): Map<String, Any?> {
val thermalStatus = if (Build.VERSION.SDK_INT >= 29) {
powerManager.currentThermalStatus
} else {
null
}
val chargeCounter = if (Build.VERSION.SDK_INT >= 21) {
batteryManager.getLongProperty(
BatteryManager.BATTERY_PROPERTY_CHARGE_COUNTER
).let { if (it == Long.MIN_VALUE) null else it }
} else {
Long.MIN_VALUE
}
return mapOf(
"ts_ms" to System.currentTimeMillis(),
"battery_level_pct" to batteryManager.getIntProperty(
BatteryManager.BATTERY_PROPERTY_CAPACITY
),
"charge_counter_uah" to chargeCounter,
"thermal_status" to thermalStatus
)
}
}
GPU 온도는 Android 공개 API만으로 모든 기기에서 동일하게 읽을 수 없다. 디바이스 팜 에이전트에서 dumpsys thermalservice 또는 제조사별 센서 경로를 어댑터로 감싸되 기기 모델과 OS 빌드를 키로 삼아 파서를 분리하는 편이 안전하다.
adb -s SERIAL shell dumpsys battery
adb -s SERIAL shell dumpsys thermalservice
adb -s SERIAL shell dumpsys gfxinfo PACKAGE framestats
dumpsys 출력 형식과 센서 이름은 기기마다 다를 수 있으므로 문자열 파싱 결과를 그대로 합격 처리에 사용하지 말고 파싱 성공 여부와 원문 로그도 함께 보관한다.

3단계: 샘플을 정규화하고 자동 판정한다
모든 이벤트는 테스트 시작 시각을 기준으로 상대 시간으로 변환한다. 1초 간격으로 수집하더라도 순간적인 센서 오류가 있을 수 있으므로 5초 또는 10초 이동 평균을 계산한다.
예시 정책은 다음과 같다. 수치는 보편적인 표준이 아니라 프로젝트의 기준선을 만들기 위한 초기값이다.
| 판정 항목 | 예시 조건 | 처리 |
|---|---|---|
| 배터리 절대 소모 | R_battery > 6%/h | 경고 또는 실패 |
| 배터리 회귀 | 기준선 대비 30% 이상 증가 | 성능 회귀 실패 |
| GPU 온도 | gpu_temp_c >= 85°C가 30초 지속 | 실패 |
| 시스템 열 상태 | thermal_status >= 3이 10초 지속 | 실패 또는 재현 보류 |
| 프레임 영향 | frame_time_p95_ms가 기준선보다 20% 이상 증가 | 원인 분석 태그 추가 |
판정 코드는 다음처럼 연속 지속 시간을 확인해야 한다.
def classify(samples, battery_limit_pct_h=6.0,
gpu_limit_c=85.0, thermal_limit=3,
thermal_hold_sec=10, gpu_hold_sec=30):
if len(samples) < 2:
return {"status": "insufficient_data"}
elapsed_h = (samples[-1]["ts_ms"] - samples[0]["ts_ms"]) / 3600000
battery_rate = 0.0
if elapsed_h > 0:
battery_rate = (
samples[0]["battery_level_pct"] -
samples[-1]["battery_level_pct"]
) / elapsed_h
hot_since = None
thermal_since = None
max_gpu_temp = None
gpu_violation = False
thermal_violation = False
for sample in samples:
timestamp = sample["ts_ms"]
gpu_temp = sample.get("gpu_temp_c")
is_hot = gpu_temp is not None and gpu_temp >= gpu_limit_c
is_thermal = sample.get("thermal_status", 0) >= thermal_limit
if gpu_temp is not None:
max_gpu_temp = gpu_temp if max_gpu_temp is None else max(max_gpu_temp, gpu_temp)
hot_since = timestamp if is_hot and hot_since is None else hot_since
hot_since = None if not is_hot else hot_since
thermal_since = timestamp if is_thermal and thermal_since is None else thermal_since
thermal_since = None if not is_thermal else thermal_since
if hot_since is not None and timestamp - hot_since >= gpu_hold_sec * 1000:
gpu_violation = True
if thermal_since is not None and timestamp - thermal_since >= thermal_hold_sec * 1000:
thermal_violation = True
reasons = []
if battery_rate > battery_limit_pct_h:
reasons.append("battery_drain_rate")
if gpu_violation:
reasons.append("gpu_temperature_sustained")
if thermal_violation:
reasons.append("thermal_status_sustained")
return {
"status": "failed" if reasons else "passed",
"battery_rate_pct_h": round(battery_rate, 2),
"max_gpu_temp_c": max_gpu_temp,
"reasons": reasons
}
실제 운영에서는 절대 임계치와 기준선 회귀를 함께 사용한다. 예를 들어 같은 모델·OS 빌드에서 최근 10회 중앙값이 4.0%/h라면 5.2%/h 이상을 회귀 후보로 볼 수 있다. 다만 기준선이 너무 적거나 온도 조건이 달라진 실행은 기준선에 포함하지 않아야 한다.
자동 탐지 파이프라인은 어떻게 구성할까?
flowchart LR
A[테스트 시나리오 실행] --> B[디바이스 에이전트]
B --> C[배터리 샘플]
B --> D[열 상태 GPU 프레임 샘플]
C --> E[정규화 및 이동 평균]
D --> E
E --> F[기기별 기준선 비교]
F --> G{지속 시간 임계치 초과}
G -->|예| H[CI 실패 원인과 원시 로그 저장]
G -->|아니오| I[통과 또는 관찰 결과 저장]
파이프라인은 다음 순서로 나누면 운영하기 쉽다.
- 실행기가
run_id를 발급하고 테스트 시작 시 배터리·온도·충전 상태를 저장한다. - 에이전트가 1~5초 간격으로 공통 이벤트를 전송한다.
- 수집기가 단위와 타임스탬프를 정규화하고 누락 샘플을 표시한다.
- 판정기가 절대 임계치, 지속 시간, 기준선 회귀를 계산한다.
- CI가 실패 이유와 함께 원시 이벤트,
logcat,dumpsys, 프레임 통계, 테스트 영상을 아티팩트로 보관한다.
열 상태가 임계치를 넘었다고 바로 테스트를 재시작하면 원인 로그를 잃을 수 있다. 먼저 샘플링을 계속하고 마지막 30~60초의 원시 데이터를 저장한 뒤 종료하는 편이 분석에 유리하다. 재실행을 하더라도 원래 실패한 결과는 보존하고 재실행 결과로 덮어쓰지 않아야 한다.
오탐을 줄이는 운영 기준은 무엇인가?
워밍업과 쿨다운을 분리한다
앱 시작 직후의 셰이더 컴파일과 리소스 로딩은 정상적인 일시적 스파이크일 수 있다. 예를 들어 60초 워밍업 뒤 10분 측정 구간을 두고 결과를 계산한다. 테스트 사이에는 동일한 쿨다운 시간을 적용하고 다음 실행의 시작 온도가 허용 범위를 벗어나면 실행 자체를 invalid로 처리한다.
기기별 기준선을 관리한다
절대 온도와 배터리 소모율은 같은 모델이라도 OS 빌드, 배터리 노후도, 주변 온도에 따라 달라진다. 기준선에는 다음 메타데이터를 포함한다.
- 기기 모델과 제조사
- OS 버전과 빌드 번호
- 앱 버전과 그래픽 품질 설정
- 주변 온도와 화면 밝기
- 충전 여부와 네트워크 조건
- 기준선 생성 시각과 샘플 수
히스테리시스를 적용한다
온도가 임계치 바로 아래와 위를 반복하면 알림이 계속 발생한다. 초과 시작 조건은 85°C, 정상 복귀 조건은 80°C처럼 간격을 두고 최소 지속 시간을 적용한다. 경고와 실패를 분리하면 운영자는 즉시 차단이 필요한 문제와 추적이 필요한 문제를 구분할 수 있다.
CI와 라이브 운영에 어떻게 연결할까?
빌드 파이프라인에서는 다음 조건을 권장한다.
- 배터리 절대 소모율 초과: 실패
- 기준선 대비 배터리 소모 30% 이상 증가: 회귀 실패
- GPU 온도 임계치 30초 지속: 실패
thermal_status >= 310초 지속: 실패- 센서 누락: 자동 통과가 아니라
unknown또는 별도 관찰 결과
운영 대시보드에는 최대값만 표시하지 말고 시간축 그래프를 제공해야 한다. 배터리 소모율, GPU 온도, 열 상태, GPU 사용률, 프레임 시간의 변화를 같은 축으로 확인하면 발열이 원인인지 결과인지 구분하기 쉽다.
알림 메시지는 다음 정보를 포함해야 한다.
{
"status": "failed",
"run_id": "run-20260824-001",
"device": "example-device",
"reasons": ["gpu_temperature_sustained", "battery_drain_regression"],
"metrics": {
"battery_rate_pct_h": 7.4,
"baseline_rate_pct_h": 4.1,
"max_gpu_temp_c": 87.2,
"thermal_status_max": 3
},
"artifacts": ["raw-samples.json", "thermalservice.txt", "framestats.txt"]
}
자주 묻는 질문 (FAQ)
GPU 온도 센서가 없는 기기는 어떻게 판정하나요?
gpu_temp_c를 억지로 추정하지 말고 thermal_status, GPU 사용률, GPU 클럭, 프레임 시간, 배터리 전류를 조합한다. 직접 온도를 확인할 수 없다는 사실은 결과에 sensor_unavailable로 표시하고 별도 커버리지 지표로 관리한다.
배터리 퍼센트가 실행 중 갑자기 올라가면 어떻게 하나요?
배터리 게이지 보정이나 충전 상태 변화를 의심해야 한다. 충전 여부를 확인하고 charge_counter_uah와 전류를 우선 사용한다. 퍼센트가 비단조적으로 변한 실행은 자동 실패보다 invalid_measurement로 분류하는 편이 안전하다.
GPU 온도 85°C를 모든 기기에 적용해도 되나요?
아니다. 85°C는 예시 시작값이다. 제조사 열 정책과 기기별 기준선을 확인하고 온도 절대값, Android Thermal API 상태, 프레임 성능 저하를 함께 사용해 임계치를 조정해야 한다.
정리
모바일 디바이스 팜의 배터리와 GPU 발열 자동 탐지는 센서 하나를 읽는 기능이 아니라 재현 가능한 테스트 조건, 공통 이벤트 스키마, 기기별 기준선, 지속 시간 기반 판정의 조합이다. BatteryManager와 PowerManager를 기본 계층으로 사용하고 벤더별 GPU 센서는 어댑터로 격리하면 기기 확장과 CI 운영을 동시에 안정화할 수 있다. 최종 결과에는 합격 여부뿐 아니라 원시 샘플과 열 상태 변화까지 남겨야 성능 회귀와 실제 발열 버그를 빠르게 구분할 수 있다.



