diff --git a/app/src/main/java/com/sw/dualscreen/activity/MainActivity.kt b/app/src/main/java/com/sw/dualscreen/activity/MainActivity.kt index a51c408..c5a8dd7 100644 --- a/app/src/main/java/com/sw/dualscreen/activity/MainActivity.kt +++ b/app/src/main/java/com/sw/dualscreen/activity/MainActivity.kt @@ -1297,7 +1297,24 @@ class MainActivity : BaseActivity() { startTime = System.currentTimeMillis() log("registerDataChange photoUri bitmap保存文件路径:${file?.absolutePath}") //val nameList = FoodModule.queryFood(bitmap) - val scoreList = FoodModule.getFoodScoreList(bitmap) + // 推理前同时暂停主屏相机和副屏人脸识别,让 ArcSoft 线程进入空闲后再启动 PyTorch + withContext(Dispatchers.Main) { + shutdownCamera() + presentation?.pauseCamera() + } + // 等待相机 HAL 在内核层(uvcvideo URB / DMC system_status)完成异步清理 + // 实测:unbindAll() 之后 HAL 仍会在 ~200ms 内写 sysfs_dmc/system_status, + // 若此时 PyTorch forward() 同时占用大量 DRAM 带宽,RK3588 DMC 频率切换 + // 与高带宽访问并发会触发硬件级故障导致设备重启。 + // 1500ms 给 HAL 足够时间完成 DMC 带宽 hold 释放后再启动推理。 + Timber.tag(TAG).d("queryFoodData: 等待相机HAL完成DMC清理 (1500ms)...") + kotlinx.coroutines.delay(1500) + Timber.tag(TAG).d("queryFoodData: 延迟结束,开始 forward()") + val scoreList = try { + FoodModule.getFoodScoreList(bitmap) + } finally { + withContext(Dispatchers.Main) { setupCamera() } + } val foodName = if (scoreList.isNotEmpty()) { val recDataJson = GsonUtils.toJson(scoreList) log("registerDataChange main,getFoodScoreList耗时:${System.currentTimeMillis() - startTime}") diff --git a/app/src/main/java/com/sw/dualscreen/network/api/ApiServiceV2.kt b/app/src/main/java/com/sw/dualscreen/network/api/ApiServiceV2.kt index 7fdc538..d36394c 100644 --- a/app/src/main/java/com/sw/dualscreen/network/api/ApiServiceV2.kt +++ b/app/src/main/java/com/sw/dualscreen/network/api/ApiServiceV2.kt @@ -36,13 +36,13 @@ interface ApiServiceV2 { @POST suspend fun getFacePage( - @Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/serve/face/page", + @Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/common/face/page", @Body request: Map ): ApiResponse?> @POST suspend fun getFaceIncrement( - @Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/serve/face/increment", + @Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/common/face/increment", @Body request: Map ): ApiResponse?> diff --git a/app/src/main/java/com/sw/dualscreen/objbox/FoodModule.kt b/app/src/main/java/com/sw/dualscreen/objbox/FoodModule.kt index 3ffd434..855802c 100644 --- a/app/src/main/java/com/sw/dualscreen/objbox/FoodModule.kt +++ b/app/src/main/java/com/sw/dualscreen/objbox/FoodModule.kt @@ -15,6 +15,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.pytorch.IValue import org.pytorch.Module +import org.pytorch.Tensor import org.pytorch.torchvision.TensorImageUtils import timber.log.Timber import java.io.File @@ -29,6 +30,9 @@ object FoodModule { // private const val THRESHOLD = 0.0 private var module: Module? = null + // 专用推理线程:与 Kotlin IO 调度器完全隔离,避免 ArcSoft 污染该线程的 FPU 状态(FPSCR) + private val inferenceExecutor = java.util.concurrent.Executors.newSingleThreadExecutor() + // private lateinit var embeddingsList: List> // private lateinit var labelsList: IntArray // private lateinit var classInfo: FoodClassInfo @@ -46,6 +50,23 @@ object FoodModule { withContext(Dispatchers.IO) { val modelPath = copyAssetToCache(context, "best_embedding_model_mobile.pt") module = Module.load(modelPath) + // 预热:在相机启动前先跑一次 forward,触发 PyTorch 线程池创建 + // 在预热前将当前线程优先级调低,使 PyTorch 内部新建的工作线程也继承低优先级 + // 从而降低后续推理时的瞬间功率,避免触发 PMIC 过流保护 + Timber.tag(TAG).d("预热 forward() 开始") + val warmupTensor = Tensor.fromBlob( + FloatArray(1 * 3 * 224 * 224), + longArrayOf(1, 3, 224, 224) + ) + inferenceExecutor.submit { module?.forward(IValue.from(warmupTensor)) }.get() + context.let { ctx -> + val am = ctx.getSystemService(android.content.Context.ACTIVITY_SERVICE) + as android.app.ActivityManager + val mi = android.app.ActivityManager.MemoryInfo() + am.getMemoryInfo(mi) + Timber.tag(TAG).d("预热完成后系统可用内存: ${mi.availMem / 1024 / 1024} MB / 总: ${mi.totalMem / 1024 / 1024} MB") + } + Timber.tag(TAG).d("预热 forward() 完成") //初始化默认重新拉取数据,先清空本地数据 val list = ObjectBox.getAll() if (list.isNotEmpty()) { @@ -86,8 +107,28 @@ object FoodModule { if (module == null) { return null } - val outputTensor = module?.forward(IValue.from(inputTensor))?.toTensor() - return outputTensor?.dataAsFloatArray + Timber.tag(TAG).d("module:${module}") + val iValue = IValue.from(inputTensor) + Timber.tag(TAG).d("inputTensor:${inputTensor}") + + // 推理前记录系统可用内存,排查是否因内存不足触发 OOM 重启 + MyApp.instance?.let { ctx -> + val am = ctx.getSystemService(android.content.Context.ACTIVITY_SERVICE) + as android.app.ActivityManager + val mi = android.app.ActivityManager.MemoryInfo() + am.getMemoryInfo(mi) + Timber.tag(TAG).d("forward() 前系统可用内存: ${mi.availMem / 1024 / 1024} MB / 总: ${mi.totalMem / 1024 / 1024} MB, lowMemory=${mi.lowMemory}") + } + Timber.tag(TAG).d("forward() 开始: ${System.currentTimeMillis()}") + val outputIValue = inferenceExecutor.submit { module?.forward(iValue) } + .get(10, java.util.concurrent.TimeUnit.SECONDS) ?: return null + Timber.tag(TAG).d("forward() 结束: ${System.currentTimeMillis()}") + + + Timber.tag(TAG).d("outputIValue:${outputIValue}") + val outputTensor = outputIValue.toTensor() + Timber.tag(TAG).d("outputTensor:${outputTensor}") + return outputTensor.dataAsFloatArray } catch (e: OutOfMemoryError) { e.printStackTrace() } finally {