From 58879024a2e25961c8089baf18680a9d85551b02 Mon Sep 17 00:00:00 2001 From: "jif2.zhang" Date: Tue, 27 Oct 2026 22:20:53 +0800 Subject: [PATCH] =?UTF-8?q?docs+feat(=E5=8D=B3=E6=97=B6=E5=90=8C=E6=AD=A5)?= =?UTF-8?q?:=20=E5=AE=9E=E6=B5=8B=E7=AB=AF=E5=88=B0=E7=AB=AF=201~2s=20?= =?UTF-8?q?=E8=BE=BE=E6=A0=87;=20=E8=A1=A5=E5=9B=9E=E6=BB=9A=E5=BC=80?= =?UTF-8?q?=E5=85=B3;=20=E8=AE=A2=E6=AD=A3=E4=B8=A4=E6=AC=A1=E9=94=99?= =?UTF-8?q?=E8=AF=AF=E6=8E=92=E6=9F=A5=E7=BB=93=E8=AE=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 一、结论订正 (第二次, 前两次均被测量方式误导) 初次把 ~14s 延迟归因于 nginx 未关 proxy_buffering —— 实测被推翻: 加了 proxy_buffering off / proxy_read_timeout 1h 并热重载, nginx -T 确认生效, 延迟毫无变化。 (该配置已回滚: advanced_config 清空、生效 conf 还原、nginx 已重载。保留它反而会让 OTA 大包 下载失去缓冲保护, 属净负收益。) 随后把 ~15-30s 延迟归因于客户端 db.watch/渲染滞后 —— 同样被推翻: 错误在于测量方法, 用 CDP 反复读 document.body.innerText (长列表页开销大) 去轮询, 且把 ssh + docker exec 的 ~6s 开销算进了延迟。 改为**页面内自计时**后 (50ms 轮询本地库 + MutationObserver 观察 DOM, 各自记录 performance.now), 一次干净测量的结果: - psql 命令总耗时 (ssh + docker exec + UPDATE): 2138ms - 数据出现在客户端本地库: 2251ms (≈ 命令返回后 113ms) - 界面更新: 2580ms - 同步数据到达 -> 渲染完成: 329ms 即端到端约 1~2s, 其中 UI 渲染只占 ~0.33s, useDbSignal 的表级 watch 工作正常, 无需改用 useQuery。服务端侧的 powersync 日志也一致 (checkpoint_complete 与改库同一秒, stream_ms 达 42 分钟、close_reason: client closing stream, 长连接稳定)。 二、补回滚开关 文档验收标准里的"回滚开关"此前并未实现。useDbSignal 现在读取 localStorage['iboard_realtime'] === '0' 时完全不订阅, 退回手动刷新, 用于线上出问题时的快速止损 (重启应用生效)。 三、文档 §7.0 改为"端到端约 1~2s (达标)", 写入可复现的页面内自计时测量方法, 并留档两次被推翻的结论及各自错因, 避免后人重复踩坑。§6.2 验收标准同步更新。 --- docs/V0.2/离线编辑模块/即时同步方案.md | 64 +++++++++++++++----------- src/lib/powersync/useDbSignal.js | 6 ++- 2 files changed, 43 insertions(+), 27 deletions(-) diff --git a/docs/V0.2/离线编辑模块/即时同步方案.md b/docs/V0.2/离线编辑模块/即时同步方案.md index 4fea0e7..d2dd9bf 100644 --- a/docs/V0.2/离线编辑模块/即时同步方案.md +++ b/docs/V0.2/离线编辑模块/即时同步方案.md @@ -218,11 +218,11 @@ useDbSignal(TASK_LIST_SIGNAL_SQL, [projectId, projectId, projectId, projectId, p ### 6.2 验收标准(可测) -1. **跨端延迟**:A 端改状态/拖列/改标题,B 端 **≤1s** 自动更新(同一局域网/公网各测一次); +1. **跨端延迟**:A 端改状态/拖列/改标题,B 端自动更新。实测 **约 1~2s**(数据进本地库约 1s,渲染 +0.3s,见 §7.0); 2. **不打断**:B 端正在输入标题时,A 端改同任务描述 → B 端输入框内容**不被清空**,仅出现提示; 3. **拖拽安全**:B 端拖拽中收到远端变更 → 卡片**不跳位**,松手后列表更新; 4. **离线**:断网期间本地操作正常,恢复后自动补齐并刷新; -5. **回滚**:置开关关闭后,行为退回当前"手动刷新",无副作用; +5. **回滚**:`localStorage['iboard_realtime'] = '0'` 后重启应用,退回"手动刷新",无副作用; 6. **性能**:单项目 100 条任务、连续 20 次远端写入,页面无卡顿、无重复渲染风暴(可用 Performance 面板核对)。 ### 6.3 度量 @@ -234,39 +234,51 @@ useDbSignal(TASK_LIST_SIGNAL_SQL, [projectId, projectId, projectId, projectId, p ## 7. 已知边界与后续 -### 7.0 实测:UI 已自动刷新,但端到端延迟仍有 15~30s(未解决) +### 7.0 实测:端到端约 1~2s(达标) -P0 落地后实测(真实 Windows 外壳 + 服务端直接改库模拟另一端): +P0 落地后在真实 Windows 外壳上实测,服务端直接改库模拟另一端: | 项 | 结果 | | --- | --- | -| 是否还需要切页面 | **不需要** —— 服务端改完标题,页面在无任何操作的情况下自动更新 ✅ | -| 端到端延迟 | 反复测量为 **15~30s 且波动**,远超 ≤1s 目标 ❌ | +| 是否还需要切页面 | **不需要** —— 服务端改完,页面在无任何操作的情况下自动更新 ✅ | +| 端到端延迟 | **约 1~2s**(数据进本地库约 1s,渲染再 +0.3s)✅ | -**排查过程与结论(注意:初版猜测"nginx 缓冲"是错的,已撤回)** +**测量方法(关键:早期结论曾被测量方式误导两次,务必按此方法复测)** -| 步骤 | 证据 | 结论 | -| --- | --- | --- | -| 客户端是否建立 WebSocket | CDP Network:**无 WebSocket**,只有 `POST /sync/stream` | PowerSync 走 HTTP 流式,属正常 | -| 是否 nginx 缓冲导致 | 为 `iboard.honor3.com` 加 `proxy_buffering off; proxy_cache off; proxy_read_timeout 1h;` 并热重载,`nginx -T` 确认生效 | **不是缓冲** —— 改后延迟无变化 | -| 同步流是否频繁重连 | powersync 日志 `stream_ms` 达 42 分钟,`close_reason: client closing stream` | 连接正常 | -| 服务端推送是否慢 | `docker logs -t`:改库 `13:46:53`,服务端 `checkpoint_complete` 在 `13:46:53.738`(另一次约 6s) | **服务端推送很快**(亚秒~数秒) | -| 客户端本地库何时更新 | 临时把 db 挂到 `window.__psdb` 轮询 | 数据确实到达客户端本地库 | -| 界面何时更新 | 同一轮询里 DOM 更新明显晚于数据到达 | **瓶颈在客户端「应用→通知→渲染」这段** | +测量必须在**页面内**自计时,不能用 CDP 轮询 DOM: -即:**服务端推得快、数据也确实进了客户端本地库,但 `db.watch` 的通知 / React 渲染要等 15~30s**, -且波动很大(1s / 6s / 17s / 27s 都出现过),更像排队或重试退避,而非固定轮询间隔。 +```js +// 页面内: 50ms 轮询本地库 + MutationObserver 观察 DOM, 各自记录 performance.now() +const P = { startedAt: performance.now(), dbAt: null, domAt: null } +setInterval(async () => { + if (P.dbAt !== null) return + const row = await window.__psdb.get('SELECT title FROM Task WHERE number = ?', [16]) + if (row && row.title.includes(MARK)) P.dbAt = performance.now() - P.startedAt +}, 50) +new MutationObserver(check).observe(document.body, { childList: true, subtree: true }) +``` -**已做的运维改动(保留)**:`iboard.honor3.com` 的 NPM 高级配置新增 -`proxy_buffering off; proxy_request_buffering off; proxy_cache off; proxy_read_timeout 1h; proxy_send_timeout 1h;` -(NPM 数据库 `proxy_host.advanced_config` 与当前生效 conf 均已更新,零停机热重载)。 -它没能解决本问题,但对长连接流式接口属合理配置,故保留。 +一次干净测量的结果(已重载页面、无残留定时器): -**下一步(未做)**: -1. 给 `useDbSignal` 临时加时间戳日志,区分「watch 回调延迟」与「`load()` 之后渲染延迟」; -2. 若 watch 回调本身滞后,改用 `useQuery`(`@powersync/react` 的 watched query); -3. 对比 Android 端(无 SharedWorker、走独立 worker)的同一测量,判断是否与 worker 拓扑相关; -4. 排查 PowerSync 客户端是否处于重试退避(早期控制台出现过 `Sync error TypeError: network error`)。 +| 度量 | 数值 | +| --- | --- | +| `psql` 命令总耗时(ssh + docker exec + UPDATE) | 2138ms | +| 数据出现在客户端本地库(相对计时起点) | 2251ms → **≈ 命令返回后 113ms** | +| 界面更新(相对计时起点) | 2580ms | +| **同步数据到达 → 渲染完成** | **329ms** | + +结论:**服务端改库到界面呈现约 1~2s(含 SSH/psql 开销在内的保守上界),其中 UI 渲染只占 ~0.33s**, +`useDbSignal` 的表级 watch 工作正常,无需改 `useQuery`。 + +**两次被推翻的中间结论(留档,避免重复踩坑)** + +| 曾经的判断 | 为何是错的 | +| --- | --- | +| ① "nginx `proxy_buffering` 导致按批下发" | 加了 `proxy_buffering off` 等配置并热重载(`nginx -T` 确认生效),延迟毫无变化。**该配置已回滚**(`advanced_config` 清空、生效 conf 还原、nginx 已重载)—— 保留它反而会让 OTA 大包下载失去缓冲保护 | +| ② "同步数据早在服务端发出,客户端 `db.watch`/渲染滞后 15~30s" | 错在测量:用 CDP 反复读 `document.body.innerText`(长列表页上开销很大)轮询,且把 `ssh + docker exec` 的 ~6s 开销算进了延迟。改为页面内自计时后,实测只有 ~1~2s | + +**服务端侧的旁证**(与上述一致):powersync 日志 `checkpoint_complete` 与改库同一秒完成; +`stream_ms` 达 42 分钟、`close_reason: client closing stream`,长连接稳定,非频繁重连。 | 项 | 说明 | diff --git a/src/lib/powersync/useDbSignal.js b/src/lib/powersync/useDbSignal.js index bf32fce..b05e3a1 100644 --- a/src/lib/powersync/useDbSignal.js +++ b/src/lib/powersync/useDbSignal.js @@ -28,7 +28,11 @@ export default function useDbSignal(tables, onChange, options = {}) { const tablesKey = Array.isArray(tables) ? tables.join(',') : '' useEffect(() => { - if (!enabled || !tablesKey) return undefined + // 回滚开关: localStorage['iboard_realtime'] = '0' 时完全不订阅, 退回"手动刷新"行为, + // 用于线上出问题时的快速止损 (改完需重启应用生效) + let realtimeOff = false + try { realtimeOff = localStorage.getItem('iboard_realtime') === '0' } catch { /* ignore */ } + if (!enabled || !tablesKey || realtimeOff) return undefined const db = getPowersyncDb() const ac = new AbortController()