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