QA round 1 (commit 9a9eb2a) FAIL 시 누락된 두 AC 보강.
AC-6: device_info_plus 만으론 4GB 임계 측정 불가 (isLowRamDevice 는
~1GB 기준). MethodChannel `life_helper/device_caps` 신설 + MainActivity.kt
에서 ActivityManager.MemoryInfo.totalMem 노출. data/ai/device_capabilities.dart
는 DeviceCapabilities abstract + PlatformDeviceCapabilities + 4 GiB
임계. deviceMeetsAiRamProvider (FutureProvider<bool>, fail-closed).
SettingsScreen 토글 disabled + "RAM 부족" 안내 (RAM < 4GB).
AC-10: docs/reference/215-ai-frame-suggest.md 의 OQ-1/placeholder
6곳을 실 구현 표현으로 갱신. §8 알려진 제약 = AC-6 device gate +
AC-7 실 단말 E2E + F1 unload + #221 corpus 평가. §9 다음 단계 =
#219~#222 follow-up 목록. 신규 테스트 합계 41 / 전체 88 통과.
테스트: device_capabilities_test.dart 7 신규 (kAiMinRamBytes 동등,
null/0/3.9GB/4GB-1/4GB/8GB 경계). flutter analyze 무이슈, 전체 88 통과
(71 기존 + 10 gemma + 7 RAM gate).
Architect 설계서 §4 의 "RAM 4GB 차단 = AC-9 재활용" 문구는 사실 #215
미구현 사항이라 본 라운드에서 신규 추가했음을 README 에 명기.
Refs #218
59 lines
1.9 KiB
Dart
59 lines
1.9 KiB
Dart
import 'dart:io';
|
|
|
|
import 'package:flutter/services.dart';
|
|
|
|
/// Minimum RAM (bytes) required for on-device Gemma 4 E2B inference.
|
|
///
|
|
/// 4 GiB matches Planner AC-6 of #218. The Gemma 4 E2B weights alone are
|
|
/// ~2.4GB; adding KV-cache + Flutter runtime + OS headroom puts us at 4GB
|
|
/// total as the practical floor below which AC-7 cold-start budgets fail.
|
|
const int kAiMinRamBytes = 4 * 1024 * 1024 * 1024;
|
|
|
|
/// Abstraction over the platform-channel RAM query, so tests can inject a
|
|
/// fake without touching MethodChannel.
|
|
abstract class DeviceCapabilities {
|
|
/// Returns total physical RAM in bytes, or `null` if unknown / unsupported
|
|
/// (non-Android host, channel error). Callers must treat `null` as "do
|
|
/// not enable the AI gate" (fail-closed).
|
|
Future<int?> totalRamBytes();
|
|
|
|
/// Convenience: `true` iff [totalRamBytes] returns ≥ [kAiMinRamBytes].
|
|
/// `null` from [totalRamBytes] → `false` (fail-closed).
|
|
Future<bool> meetsAiMinRam() async {
|
|
final bytes = await totalRamBytes();
|
|
if (bytes == null) return false;
|
|
return bytes >= kAiMinRamBytes;
|
|
}
|
|
}
|
|
|
|
/// Real implementation. Calls `MainActivity.kt` over a MethodChannel.
|
|
class PlatformDeviceCapabilities implements DeviceCapabilities {
|
|
PlatformDeviceCapabilities({MethodChannel? channel})
|
|
: _channel = channel ??
|
|
const MethodChannel('life_helper/device_caps');
|
|
|
|
final MethodChannel _channel;
|
|
|
|
@override
|
|
Future<int?> totalRamBytes() async {
|
|
// Channel is Android-only — return null on iOS/host tests rather than
|
|
// throwing MissingPluginException.
|
|
if (!Platform.isAndroid) return null;
|
|
try {
|
|
final v = await _channel.invokeMethod<int>('totalMemoryBytes');
|
|
return v;
|
|
} on PlatformException {
|
|
return null;
|
|
} on MissingPluginException {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
@override
|
|
Future<bool> meetsAiMinRam() async {
|
|
final bytes = await totalRamBytes();
|
|
if (bytes == null) return false;
|
|
return bytes >= kAiMinRamBytes;
|
|
}
|
|
}
|