perf(activity): 优化秤数据 StateFlow 重发时的无效遍历,添加 ts 去重跳过未变化帧;ScaleWebSocketServer 改为 trailing 节流并增加数据变化去重

This commit is contained in:
2026-05-12 14:07:22 +08:00
parent b6389be898
commit d412e03c6e
2 changed files with 77 additions and 20 deletions
@@ -10,6 +10,7 @@ import org.java_websocket.handshake.ClientHandshake
import org.java_websocket.server.WebSocketServer
import java.net.InetSocketAddress
import java.util.concurrent.Executors
import java.util.concurrent.ScheduledFuture
import java.util.concurrent.TimeUnit
/**
@@ -50,6 +51,9 @@ class ScaleWebSocketServer(private val deviceId: String, private val context: Co
/** 缓存各秤最新数据,新客户端连接时立即同步当前状态 */
private val latestData = mutableMapOf<Int, ScaleData>()
/** 节流窗口内的延迟推送任务,key=address;保证窗口结束时最新数据被推送 */
private val pendingPushTasks = mutableMapOf<Int, ScheduledFuture<*>>()
/**
* 启动 WebSocket 服务器,并注册 WeightUtil 回调
*/
@@ -61,11 +65,12 @@ class ScaleWebSocketServer(private val deviceId: String, private val context: Co
// 注册本机秤数据监听
WeightUtil.addWeightListener(TAG) { address, state, weight ->
Log.d(TAG, "WebSocket 设备${deviceId}推送数据, address=$address, state=$state, weight=$weight")
val now = System.currentTimeMillis()
// 节流:同一地址 100ms 内只推送一次
if (now - (lastPushTime[address] ?: 0L) < THROTTLE_MS) return@addWeightListener
// 方案三:数据未变化时直接跳过,避免无效推送
val last = latestData[address]
if (last?.weight == weight && last.state == state) return@addWeightListener
lastPushTime[address] = now
val data = ScaleData(
deviceId = deviceId,
address = address,
@@ -74,8 +79,28 @@ class ScaleWebSocketServer(private val deviceId: String, private val context: Co
ts = now,
ip = NetworkUtil.getLocalIpAddress(context)
)
// 始终更新 latestData,保证最新一帧不丢失
latestData[address] = data
broadcast(gson.toJson(data))
val lastPush = lastPushTime[address] ?: 0L
if (now - lastPush >= THROTTLE_MS) {
// 窗口已过:立即推送,并取消可能存在的延迟任务
lastPushTime[address] = now
pendingPushTasks.remove(address)?.cancel(false)
broadcast(gson.toJson(data))
} else if (pendingPushTasks[address] == null) {
// 方案一:窗口内首次触发时安排一个 trailing 推送,保证窗口末尾推最新值
val delay = THROTTLE_MS - (now - lastPush)
if (!scheduler.isShutdown) {
pendingPushTasks[address] = scheduler.schedule({
val latest = latestData[address] ?: return@schedule
lastPushTime[address] = System.currentTimeMillis()
pendingPushTasks.remove(address)
broadcast(gson.toJson(latest))
}, delay, TimeUnit.MILLISECONDS)
}
}
// 窗口内且已安排过 trailing 任务:仅更新 latestData,等 trailing 触发时推送
}
}
@@ -96,6 +121,8 @@ class ScaleWebSocketServer(private val deviceId: String, private val context: Co
server = null
latestData.clear()
lastPushTime.clear()
pendingPushTasks.values.forEach { it.cancel(false) }
pendingPushTasks.clear()
Log.d(TAG, "WebSocket 服务端已停止")
}
@@ -95,6 +95,11 @@ class SubmitFoodActivity : BaseActivity() {
*/
private val baseSeasoningMap = mutableMapOf<String, Pair<String, Double>>()
/**
* 各秤上一次处理过的 ts,用于跳过 StateFlow 因其他秤变化而重发的相同帧
*/
private val lastTsMap = mutableMapOf<String, Long>()
@Suppress("unchecked_cast", "DEPRECATION")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -175,7 +180,8 @@ class SubmitFoodActivity : BaseActivity() {
// 制作中:从数据库查询 goodsList,筛选调料数据预填充 adapter
val foodId = food?.foodId ?: return@launch
val cookMode = food?.cookMode ?: return@launch
val list = appViewModel.getCookFoodGoodsList(foodId, cookMode, food?.dinnerType ?: "0")
val list =
appViewModel.getCookFoodGoodsList(foodId, cookMode, food?.dinnerType ?: "0")
goodsList = list
prefillSeasoningFromGoodsList(list)
} else {
@@ -213,13 +219,20 @@ class SubmitFoodActivity : BaseActivity() {
val flow = ScaleServiceManager.allScales ?: return
lifecycleScope.launch {
flow.collectLatest { scaleMap ->
//Log.d(TAG, "observeScaleData,scaleMap=${scaleMap.toJsonString()}")
scaleMap.forEach { (key, data) ->
// ts 未变化说明是其他秤更新触发的 StateFlow 重发,跳过
if (lastTsMap[key] == data.ts) return@forEach
lastTsMap[key] = data.ts
// 排除主设备 2格秤
if (data.deviceId == ScaleDeviceConfig.DEVICE_ID_2) return@forEach
// 从槽位配置读取调料名,未配置则跳过
val (goodsId, goodsName) = slotMap[key] ?: return@forEach
Log.d(
TAG,
"observeScaleData,1key=$key, goodsId=$goodsId, goodsName=$goodsName, data=$data"
)
// 首次收到稳定数据时记录基准重量
val baseline = baselineMap[key]
if (baseline == null) {
@@ -228,19 +241,28 @@ class SubmitFoodActivity : BaseActivity() {
}
return@forEach
}
Log.d(TAG, "observeScaleData,2key=$key, baseline=$baseline, data.weight=${data.weight}")
// 用量 = 初始重量 - 当前重量,负值归零
val useWeight = (baseline - data.weight)
.coerceAtLeast(0.0)
.roundedOneDecimalPlace()
// 更新原始用量 map,再按 goodsName 聚合刷新 adapter
if (useWeight < 0.5) {
rawWeightMap.remove(key)
// 放回场景:必须稳定后才移除,避免拿取过程中误删
if (data.state != WeightUtil.STATE_STABLE) return@forEach
if (rawWeightMap.remove(key) != null) {
Log.d(TAG, "observeScaleData,移除key=${key}, goodsName=$goodsName")
refreshAdapterByName(goodsName)
}
} else {
rawWeightMap[key] = Triple(goodsId, goodsName, useWeight)
// 拿取场景:实时响应,无需等待稳定
val newTriple = Triple(goodsId, goodsName, useWeight)
if (rawWeightMap[key] != newTriple) {
Log.d(TAG, "observeScaleData,新增:$newTriple")
rawWeightMap[key] = newTriple
refreshAdapterByName(goodsName)
}
}
refreshAdapterByName(goodsName)
}
}
}
@@ -249,27 +271,35 @@ class SubmitFoodActivity : BaseActivity() {
/**
* 按 goodsName 聚合 rawWeightMap,叠加历史用量后推给 adapter
* isCooking=true 时只新增,不移除(历史调料数据不可减少)
* 移除前检查 item 是否存在,更新前比较数据是否变化,避免无效操作
*/
private fun refreshAdapterByName(goodsName: String) {
val grouped = rawWeightMap.values.filter { it.second == goodsName }
val newWeight = grouped.sumOf { it.third }.roundedOneDecimalPlace()
val (baseGoodsId, baseWeight) = baseSeasoningMap[goodsName] ?: ("" to 0.0)
val totalWeight = (newWeight + baseWeight).roundedOneDecimalPlace()
Log.d(TAG, "refreshAdapterByName, goodsName=$goodsName, newWeight=$newWeight, baseWeight=$baseWeight, totalWeight=$totalWeight, isCooking=${food?.isCooking}")
if (totalWeight < 0.5) {
// 制作中模式:已有历史数据,不允许移除
if (food?.isCooking != true) {
seasoningAdapter.removeItem(goodsName)
Log.d(TAG, "refreshAdapterByName,重量小于0.5g, isCooking=${food?.isCooking}, goodsName=$goodsName, totalWeight=$totalWeight")
// 仅保护 baseSeasoningMap 中的历史调料;本次会话新加的调料可以移除
if (!baseSeasoningMap.containsKey(goodsName)) {
// 只有 item 确实存在时才调用 remove,避免无效遍历
if (seasoningAdapter.items.any { it.goodsName == goodsName }) {
seasoningAdapter.removeItem(goodsName)
}
}
return
}
val goodsId = grouped.firstOrNull()?.first?.takeIf { it.isNotEmpty() } ?: baseGoodsId
seasoningAdapter.updateItem(
SeasoningWeightAdapter.Item(
goodsId = goodsId,
goodsName = goodsName,
useWeight = totalWeight
)
// 数据未变化时跳过,避免触发不必要的 RecyclerView 刷新
if (seasoningAdapter.items.any { it.goodsName == goodsName && it.useWeight == totalWeight && it.goodsId == goodsId }) return
val item = SeasoningWeightAdapter.Item(
goodsId = goodsId,
goodsName = goodsName,
useWeight = totalWeight
)
Log.d(TAG, "refreshAdapterByName,更新数据item=${item.toJsonString()}")
seasoningAdapter.updateItem(item)
}
private fun addViewListener() {