perf(activity): 优化秤数据 StateFlow 重发时的无效遍历,添加 ts 去重跳过未变化帧;ScaleWebSocketServer 改为 trailing 节流并增加数据变化去重
This commit is contained in:
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user