feat: 添加流控机制以优化帧数据发送,增强稳定性和性能监控

This commit is contained in:
康猛 2026-02-02 14:04:34 +08:00
parent e320bc29a2
commit 441ff21996
3 changed files with 149 additions and 31 deletions

View File

@ -36,6 +36,7 @@ export default ({ config }) => {
ios: { ios: {
supportsTablet: true, supportsTablet: true,
infoPlist: { infoPlist: {
UIViewControllerBasedStatusBarAppearance: false,
NSBluetoothPeripheralUsageDescription: NSBluetoothPeripheralUsageDescription:
'This app uses Bluetooth to act as a peripheral device for testing and development purposes.', 'This app uses Bluetooth to act as a peripheral device for testing and development purposes.',
CFBundleURLTypes: [ CFBundleURLTypes: [

View File

@ -31,12 +31,18 @@ import { screenWidth, uploadFile } from '@/utils'
import { cn } from '@/utils/cn' import { cn } from '@/utils/cn'
import { extractCdnKey } from '@/utils/getCDNKey' import { extractCdnKey } from '@/utils/getCDNKey'
const ITEM_WIDTH = Math.floor((screenWidth - 12 * 2 - 12 * 2) / 3) const ITEM_GAP = 6
// FlashList 会将可用宽度(screenWidth - padding*2)平均分配给每列
// 所以每个item容器的宽度是 (screenWidth - 24) / 3
// item实际显示宽度需要减去间距
const ITEM_CONTAINER_WIDTH = Math.floor((screenWidth - 12 * 2) / 3)
const ITEM_WIDTH = ITEM_CONTAINER_WIDTH - ITEM_GAP
// ============ 主组件 ============ // ============ 主组件 ============
const Sync = observer(() => { const Sync = observer(() => {
// 从MobX Store获取用户信息 // 从MobX Store获取用户信息
const { user, isLogin } = userStore const { user, isLogin } = userStore
const insets = useSafeAreaInsets()
const { const {
data: generationsData, data: generationsData,
loading: generationsLoading, loading: generationsLoading,
@ -63,17 +69,6 @@ const Sync = observer(() => {
}) })
const [isSelectionMode, setIsSelectionMode] = useState(false) const [isSelectionMode, setIsSelectionMode] = useState(false)
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set()) const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set())
const [visibleIds, setVisibleIds] = useState<Set<string>>(new Set())
const onViewableItemsChanged = useRef(({ viewableItems }: any) => {
const ids = new Set(viewableItems.map((item: any) => item.item?.id).filter(Boolean) as string[])
setVisibleIds(ids)
}).current
const viewabilityConfig = useRef({
itemVisiblePercentThreshold: 10,
minimumViewTime: 100,
}).current
// 直接使用 useIsFocused hook页面失焦时不渲染列表项以减少内存占用 // 直接使用 useIsFocused hook页面失焦时不渲染列表项以减少内存占用
const isFocused = useIsFocused() const isFocused = useIsFocused()
@ -416,9 +411,7 @@ const Sync = observer(() => {
isSelectionMode={isSelectionMode} isSelectionMode={isSelectionMode}
itemWidth={ITEM_WIDTH} itemWidth={ITEM_WIDTH}
post={post} post={post}
// 页面失焦时不渲染,减少内存占用
onSelect={handleItemSelect} onSelect={handleItemSelect}
isVisible={visibleIds.has(post?.id)}
/> />
) )
} }
@ -427,18 +420,17 @@ const Sync = observer(() => {
return null return null
} }
// 计算底部内边距FAB按钮高度(56) + 按钮底部距离(96) + 安全区域 + 额外间距
const listPaddingBottom = 56 + 96 + insets.bottom + 20
return ( return (
<Block className="relative flex-1 bg-black"> <Block className="relative flex-1 bg-black">
<BannerSection /> <BannerSection />
<Block className="z-10 flex-1"> <Block className="z-10 flex-1">
<FlashList <FlashList
contentContainerStyle={{ paddingHorizontal: 12, paddingBottom: 200 }} contentContainerStyle={{ paddingHorizontal: 12, paddingBottom: listPaddingBottom }}
data={posts} data={posts}
// drawDistance={300}
maxItemsInRecyclePool={0}
removeClippedSubviews={true}
ItemSeparatorComponent={() => <Block style={{ height: 6 }} />}
keyExtractor={(item: any) => item?.id} keyExtractor={(item: any) => item?.id}
ListFooterComponent={ListFooter} ListFooterComponent={ListFooter}
ListHeaderComponent={renderHeader} ListHeaderComponent={renderHeader}
@ -450,8 +442,6 @@ const Sync = observer(() => {
} }
onEndReached={onLoadMore} onEndReached={onLoadMore}
onEndReachedThreshold={0.3} onEndReachedThreshold={0.3}
onViewableItemsChanged={onViewableItemsChanged}
viewabilityConfig={viewabilityConfig}
/> />
</Block> </Block>
@ -681,14 +671,12 @@ const GridItem = memo(
isSelected, isSelected,
isSelectionMode, isSelectionMode,
itemWidth, itemWidth,
isVisible = true,
onSelect, onSelect,
}: { }: {
post: any post: any
isSelected: boolean isSelected: boolean
isSelectionMode: boolean isSelectionMode: boolean
itemWidth: number itemWidth: number
isVisible?: boolean
onSelect: (post: any) => void onSelect: (post: any) => void
}) => { }) => {
// 渲染状态标记 // 渲染状态标记
@ -733,9 +721,14 @@ const GridItem = memo(
const placeholderSrc = null const placeholderSrc = null
// 新数据使用webp 旧数据使用mp4 // 新数据使用webp 旧数据使用mp4
const canShow = (post.status === 'completed' || post.status === 'success') && isVisible const canShow = post.status === 'completed' || post.status === 'success'
return ( return (
<Block className="relative" key={post?.id} onClick={() => onSelect(post)}> <Block
className="relative border border-yellow-200"
key={post?.id}
style={{ marginBottom: ITEM_GAP }}
onClick={() => onSelect(post)}
>
<Block <Block
className={`relative overflow-hidden border-2 ${isSelected ? 'shadow-[0px_0px_0px_4px_#FFE500]' : 'shadow-hard-black'} ${isSelected ? 'border-accent' : 'border-black'}`} className={`relative overflow-hidden border-2 ${isSelected ? 'shadow-[0px_0px_0px_4px_#FFE500]' : 'shadow-hard-black'} ${isSelected ? 'border-accent' : 'border-black'}`}
style={{ style={{
@ -828,7 +821,7 @@ const SelectionBar = memo(
if (!isSelectionMode) return null if (!isSelectionMode) return null
return ( return (
<Block className="absolute inset-x-[16px] bottom-[96px] z-50" style={{ paddingBottom: insets.bottom }}> <Block className="absolute inset-x-[16px] z-50" style={{ bottom: 96 + insets.bottom }}>
<Block className="-skew-x-3 flex-row items-center justify-between border-[3px] border-black bg-white p-[12px] shadow-large-black"> <Block className="-skew-x-3 flex-row items-center justify-between border-[3px] border-black bg-white p-[12px] shadow-large-black">
<Block className="skew-x-3 flex-row items-center gap-[12px] pl-[8px]"> <Block className="skew-x-3 flex-row items-center gap-[12px] pl-[8px]">
<Text className="text-[14px] font-[900]">: {selectedCount}</Text> <Text className="text-[14px] font-[900]">: {selectedCount}</Text>
@ -852,7 +845,7 @@ const SelectionBar = memo(
const FABButtons = memo(({ onGenAgain, handleSync }: { onGenAgain: () => void; handleSync: () => void }) => { const FABButtons = memo(({ onGenAgain, handleSync }: { onGenAgain: () => void; handleSync: () => void }) => {
const insets = useSafeAreaInsets() const insets = useSafeAreaInsets()
return ( return (
<Block className="absolute inset-x-[16px] bottom-[96px] z-40" style={{ paddingBottom: insets.bottom }}> <Block className="absolute inset-x-[16px] z-40" style={{ bottom: 96 + insets.bottom }}>
<Block className="w-full flex-row gap-[12px]"> <Block className="w-full flex-row gap-[12px]">
<Block className="flex-1 -skew-x-6 shadow-large-black"> <Block className="flex-1 -skew-x-6 shadow-large-black">
<Block <Block

View File

@ -193,22 +193,146 @@ export class BleProtocolService {
const frames = ProtocolManager.createFrame(type, payload, FRAME_CONSTANTS.HEAD_APP_TO_DEVICE, safeMaxDataSize) const frames = ProtocolManager.createFrame(type, payload, FRAME_CONSTANTS.HEAD_APP_TO_DEVICE, safeMaxDataSize)
const total = frames.length const total = frames.length
console.debug(`Sending ${total} frames`) console.debug(`Sending ${total} frames`)
// 使用滑动窗口流控发送
await this.sendFramesWithFlowControl(deviceId, frames, onProgress)
}
/**
* +
* +
*/
private async sendFramesWithFlowControl(
deviceId: string,
frames: Uint8Array[],
onProgress?: (progress: number) => void,
): Promise<void> {
const total = frames.length
const maxRetries = FRAME_CONSTANTS.FLOW_CONTROL.MAX_RETRIES
const retryDelay = FRAME_CONSTANTS.FLOW_CONTROL.RETRY_DELAY
// 性能监控指标
const startTime = Date.now()
let failedCount = 0
let retriedCount = 0
let totalRetries = 0
// 自适应间隔参数
const MIN_INTERVAL = 40 // 最小间隔 40ms
const MAX_INTERVAL = 150 // 最大间隔 150ms
const INITIAL_INTERVAL = 50 // 初始间隔 50ms比原来的35ms保守
let currentInterval = INITIAL_INTERVAL
let consecutiveSuccesses = 0
let consecutiveFailures = 0
console.debug(`[FlowControl] Starting serial transmission: ${total} frames, initial interval: ${currentInterval}ms`)
for (let i = 0; i < total; i++) { for (let i = 0; i < total; i++) {
const frame = frames[i] const frame = frames[i]
// 打印前几帧的原始数据用于调试
if (i < 3) { if (i < 3) {
const rawFrame = Array.from(frame) const rawFrame = Array.from(frame)
.map((b) => b.toString(16).padStart(2, '0')) .map((b) => b.toString(16).padStart(2, '0'))
.join(' ') .join(' ')
console.debug(`raw ${i + 1} frame \n ${rawFrame}`) console.debug(`raw ${i + 1} frame \n ${rawFrame}`)
} }
// console.debug(`Writing frame ${i + 1}/${total}, length = ${frame.length}`)
const base64 = Buffer.from(frame).toString('base64') const base64 = Buffer.from(frame).toString('base64')
const result = await this.client.write(deviceId, BLE_UUIDS.SERVICE, BLE_UUIDS.WRITE_CHARACTERISTIC, base64, false) let frameSucceeded = false
await new Promise((resolve) => setTimeout(resolve, FRAME_CONSTANTS.FRAME_INTERVAL))
// 重试逻辑(第一帧失败直接报错,不重试)
const frameMaxRetries = i === 0 ? 0 : maxRetries
for (let attempt = 0; attempt <= frameMaxRetries; attempt++) {
try {
// 串行等待每次写入完成
await this.client.write(deviceId, BLE_UUIDS.SERVICE, BLE_UUIDS.WRITE_CHARACTERISTIC, base64, false)
// 写入成功
frameSucceeded = true
consecutiveSuccesses++
consecutiveFailures = 0
if (attempt > 0) {
retriedCount++
totalRetries += attempt
console.debug(`[FlowControl] Frame ${i} succeeded after ${attempt} retries`)
}
// 自适应调整:连续成功 5 次,逐渐减小间隔(加速)
if (consecutiveSuccesses >= 5 && currentInterval > MIN_INTERVAL) {
currentInterval = Math.max(MIN_INTERVAL, currentInterval - 5)
console.debug(`[FlowControl] Speed up: interval reduced to ${currentInterval}ms`)
consecutiveSuccesses = 0
}
break
} catch (error) {
// 第一帧失败直接报错
if (i === 0) {
console.error(`[FlowControl] First frame write failed, aborting:`, error)
throw new Error(`First frame write failed: ${(error as Error)?.message || error}`)
}
consecutiveFailures++
consecutiveSuccesses = 0
console.warn(`[FlowControl] Frame ${i} write failed (attempt ${attempt + 1}/${frameMaxRetries + 1}):`, error)
// 如果还有重试机会,等待后重试
if (attempt < frameMaxRetries) {
const backoffDelay = retryDelay * (attempt + 1)
await new Promise((resolve) => setTimeout(resolve, backoffDelay))
} else {
// 所有重试都失败
failedCount++
console.error(`[FlowControl] Frame ${i} failed after ${frameMaxRetries} retries`)
// 严重失败:大幅增加间隔
currentInterval = Math.min(MAX_INTERVAL, currentInterval * 2)
console.warn(`[FlowControl] Severe failure: interval increased to ${currentInterval}ms`)
}
}
}
// 自适应调整:连续失败,增加间隔(退避)
if (consecutiveFailures >= 2 && currentInterval < MAX_INTERVAL) {
currentInterval = Math.min(MAX_INTERVAL, currentInterval * 1.5)
console.debug(`[FlowControl] Slow down: interval increased to ${currentInterval}ms`)
}
// 间隔等待(只有成功才继续,失败会在重试中已经等待过)
if (frameSucceeded) {
await new Promise((resolve) => setTimeout(resolve, currentInterval))
}
// 更新进度
if (onProgress) { if (onProgress) {
onProgress((i + 1) / total) onProgress((i + 1) / total)
} }
// console.debug("Wrote frame", result); }
// 性能统计
const duration = Date.now() - startTime
const throughput = total > 0 ? (total / duration) * 1000 : 0
const avgRetriesPerFailedFrame = failedCount > 0 ? totalRetries / retriedCount : 0
console.log(
`[FlowControl] Transmission completed:
- Total frames: ${total}
- Duration: ${duration}ms
- Throughput: ${throughput.toFixed(2)} frames/sec
- Failed: ${failedCount}
- Frames retried: ${retriedCount}
- Total retry attempts: ${totalRetries}
- Avg retries per failed frame: ${avgRetriesPerFailedFrame.toFixed(2)}
- Final interval: ${currentInterval}ms
- Success rate: ${(((total - failedCount) / total) * 100).toFixed(2)}%`,
)
if (failedCount > 0) {
throw new Error(`Transmission incomplete: ${failedCount}/${total} frames failed`)
} }
} }
} }