feat: 添加流控机制以优化帧数据发送,增强稳定性和性能监控
This commit is contained in:
parent
e320bc29a2
commit
441ff21996
|
|
@ -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: [
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue