v51c 卡片后台同步:修复"每轮全量重拉",单账号 236 秒 → 10 秒
真机实测根因:服务端 CalDav.php:253 用 preg_replace('/[^A-Za-z0-9._-]/','_',$uid)
派生资源文件名(UID movie-188@familybook → 资源 movie-188_familybook.ics),
而客户端 SyncEngine.syncAccountInner 拿 REPORT 给的**资源名当 uid** 去本地行里查
⇒ byUid 永远查不到 ⇒ 每轮判"变了" ⇒ 全量 GET。ETag 明明一样,比较压根执行不到。
改动:
· SyncEngine —— 加 byEtag 兜底(uid 查不到就用内容哈希认领本地行)+ 复用判定计数日志
(uid 命中 / etag 兜底命中 / 需补拉);抽 groupPush() 去重。
· EventDb.applyRemote —— existing.find() 的 O(n·m) → keyed.get() 的 O(n)。
· BackgroundSyncRunner —— runOnce 每阶段计时(+Xms)+ pid;预算可由 preferences
card_budget_ms 覆盖(诊断开关,默认走内置 9 秒)。
· AppSettings —— getCardBudgetMs()。
· EntryFormAbility —— onUpdateForm 日志带 pid。
· MirrorInbound —— 顺带修一个已死的功能:getEvents(undefined, 字段) 在 7.0.0.109 上
必抛 401 type error!,导致**入站对账 12/12 全失败**;改成"先无参、失败再带字段"
(照抄 SystemCalendarMirror.queryEvents 的绕法)。
真机验证(DEL-AL20,卡片 onUpdateForm 触发,卡片进程 pid=27411):
[21:33:06] 卡片驱动同步:开始(可用窗口 9 秒)
[21:33:09] 「观影」复用判定:uid 命中 0、etag 兜底命中 193、需补拉 0(共 193)
[21:33:09] 「观影」落库完成:不变193,耗时 1 秒 ← 修复前 124 秒
[21:33:17] 同步账号完成:拉取 2182 条,耗时 10 秒 ← 修复前 215~236 秒
[21:33:22] 后台同步[cardTick] 完成:1/1 个账号,总耗时 16 秒
12 个日历本全部跑完 + 出站镜像,9 秒窗口不再"连第一个本都过不去"。
复算(真库快照 + 真服务器 PROPFIND,_tooling/_sc_etag_sim.py):
旧逻辑补拉 1384 个 → 新逻辑 80 个(降 94.2%);观影 193→0、旅行 23→0、工作 362→5。
另证服务端 ETag == md5(正文)、三次 PROPFIND 逐字节一致 ⇒ etag 兜底语义正确、非碰巧。
This commit is contained in:
@@ -425,7 +425,22 @@ export class EventDb {
|
||||
rs.close();
|
||||
}
|
||||
|
||||
const remoteKeys: string[] = [];
|
||||
// ⭐ 2226(2026-10-15 提速):下方逐条要判"这条远端事件本地有没有对应行"。
|
||||
// 原先对每条都 `existing.find(...)` ⇒ O(n·m)(工作本 735 行 → 54 万次比较,
|
||||
// 每次还要现拼 `uid_startTime_recId` 字符串),2184 行全库累计近 100 万次。
|
||||
// 卡片后台同步的时间预算只有 9 秒,这笔开销吃不起 → 预先建 Map,改成 O(1) 查。
|
||||
// 语义与 `existing.find(x => !x.dirty && x.uid===r.uid && x.startTime===r.startTime
|
||||
// && x.recurrenceId===r.recurrenceId)` 完全一致:dirty 行不入索引、同键取**最先**出现的那行。
|
||||
const keyed: Map<string, LocalEvent> = new Map<string, LocalEvent>();
|
||||
for (const e of existing) {
|
||||
if (e.dirty) {
|
||||
continue;
|
||||
}
|
||||
const k: string = `${e.uid}_${e.startTime}_${e.recurrenceId}`;
|
||||
if (!keyed.has(k)) {
|
||||
keyed.set(k, e);
|
||||
}
|
||||
}
|
||||
// ⭐ 同一次解析里可能出现同一资源的多条相同 VEVENT(同 uid + 同 DTSTART)。
|
||||
// 旧版用循环前的 existing 快照判重,插入后不回写 → 第二条仍判为"新增"又插一行,
|
||||
// 在本地库留下 href/etag/remote_path 完全相同的冗余行(2026-09-19 实测 10 组)。
|
||||
@@ -438,10 +453,7 @@ export class EventDb {
|
||||
continue; // 同一资源内的重复条目 → 只保留一条
|
||||
}
|
||||
seenKeys.add(key);
|
||||
remoteKeys.push(key);
|
||||
const found = existing.find((x: LocalEvent): boolean =>
|
||||
!x.dirty && x.uid === r.uid && x.startTime === r.startTime
|
||||
&& x.recurrenceId === r.recurrenceId);
|
||||
const found: LocalEvent | undefined = keyed.get(key);
|
||||
if (found === undefined) {
|
||||
// 新增
|
||||
const e = new LocalEvent();
|
||||
@@ -517,7 +529,8 @@ export class EventDb {
|
||||
continue;
|
||||
}
|
||||
const localKey: string = `${local.uid}_${local.startTime}_${local.recurrenceId}`;
|
||||
if (!remoteKeys.includes(localKey)) {
|
||||
// ⭐ 同样从 O(m) 的数组 includes 换成 Set 查询(见上方 keyed 的说明)
|
||||
if (!seenKeys.has(localKey)) {
|
||||
const del = new relationalStore.RdbPredicates('events');
|
||||
del.equalTo('id', local.id);
|
||||
await store.delete(del);
|
||||
|
||||
Reference in New Issue
Block a user