ReadViewSDK/Doc/FeatureSolution/TEXT_STATISTICS_PIPELINE_OPTIMIZATION.md
shenlei 8b68975ee1 新增全书字符统计与阅读百分比,并沉淀后台渲染流水线优化方案
- 新增 RDEPUBTextStatisticsCoordinator:后台单线程按章渲染统计全书字符数,footer 显示精确阅读百分比;统计未就绪时(含 PageMap 未覆盖的 loading 页)显示"计算中",不再回退旧页码
- 统计结果按 bookID + renderSignature + 章节内容哈希做磁盘缓存,布局/字号变化自动失效;缓存命中不创建 renderer
- 移除"0.8 秒翻页时间窗轮询让路"逻辑,后续由事件驱动的交互加载门替代(见方案文档 5.6 节)
- 新增 Doc/FeatureSolution/TEXT_STATISTICS_PIPELINE_OPTIMIZATION.md:三条渲染流水线收敛为"交互 + 生产者"、交互加载门、全局后台渲染预算池、后台渲染登记表、两阶段近似百分比、自适应并发的完整优化方案与验收标准

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 18:38:41 +09:00

24 KiB
Raw Blame History

阅读进度统计与后台渲染流水线优化方案

创建2026-07-03 状态:方案评审中(两阶段近似百分比、自适应并发已确认纳入正式范围) 范围:textReflowable 阅读模式(章节级运行时)


1. 背景与问题

阅读百分比统计(RDEPUBTextStatisticsCoordinator)上线后,书籍打开初期(统计计算进行中)用户翻页明显卡顿。此前已做的缓解手段(统计降为单线程、缓存命中不创建 renderer)未根治问题。

根因不是统计线程本身,而是打开书后同时存在三条互不知晓的章节渲染流水线,它们做的是同一种重活(DTCoreText HTML → NSAttributedString 构建),同一章最多被渲染三遍,并在首屏后的前几秒集中抢占 CPU——恰好是用户开始翻页的时刻。

1.1 三条流水线现状

流水线 入口 队列 / 并发 产出 缓存
交互加载(导航/预览/预取) RDEPUBChapterLoader chapterLoadQueue(userInitiated,串行) RuntimeChapter + ChapterSummary RDEPUBChapterSummaryDiskCache
窗口元数据解析 RDEPUBMetadataParseWorker 独立 OperationQueue(utility,并发 = 全部 CPU 核) ChapterSummary + PageMap 增量刷新 同上(同 cacheKey)
全书字符统计 RDEPUBTextStatisticsWorker 独立 OperationQueue(utility,串行) 每章字符数 → 阅读百分比 RDEPUBTextStatisticsDiskCache(独立,仅字符数)

三者的缓存 key 均为同一个 RDEPUBChapterCacheKey(bookID + spineIndex + renderSignature + contentHash),但统计流水线不读 summary 缓存,summary 生产者也不通知统计。

1.2 打开书流程的具体问题

RDEPUBReaderPaginationCoordinator.paginatePublication 的执行顺序:

  1. 统计流水线最先启动(paginatePublication 内先于首章渲染调用 startTextStatistics),从 spine 0 按顺序渲染全书,与首屏关键路径抢 CPU;渲染顺序与用户阅读位置无关。
  2. 首屏串行等待 1~3 章:utility 队列上信号量同步等 anchor 章渲染;anchor 章不足 2 页时再同步加载相邻章(loadInitialInteractiveRuntimeChapters),之后才回主线程出首屏。
  3. 首屏一出即 CPU 风暴:出首屏的同一个 main.async 块内连续触发窗口 prefetch、MetadataParseWorker(全核并发)启动;统计已在跑。metadata worker 仅在启动时等一次 0.8 秒交互冷却,之后章间不让路。
  4. MetadataParseWorker 在主线程初始化:init 中同步读取覆盖窗口内每章 HTML 并计算 sha256(contentHashBySpineIndex),主线程磁盘 I/O + 哈希,首帧后主线程可见占用。
  5. 翻页卡顿的直接机制:翻到未加载章节时主线程经 loadChapterSynchronouslyForMigration 信号量同步等 chapterLoadQueue 渲染,而 CPU 已被两条全书/窗口流水线占满,该次渲染耗时被放大 2~3 倍。
  6. 架构缺口:首次打开时 MetadataParseWorker 只覆盖 anchor 附近窗口(metadataCoverageSpineIndices),全书 PageMap 靠翻页时 3 章一批扩展(extendPartialBookPageMapIfNeeded)——当前架构中不存在全书 summary 生产者,统计因此只能自己渲染全书,造成整书级别的重复劳动。

1.3 关键代码位置

  • Sources/RDReaderView/EPUBUI/ReaderController/RDEPUBReaderPaginationCoordinator.swift
    • paginatePublication:统计过早启动(约 L32)
    • paginateTextPublicationloadInitialInteractiveRuntimeChapters:首屏串行多章(L264)
    • metadataCoverageSpineIndices:窗口覆盖范围(L341)
  • Sources/RDReaderView/EPUBUI/ReaderController/RDEPUBMetadataParseWorker.swift
    • init 主线程哈希(L95~105);workerCount = activeProcessorCount(L92);启动时单次交互冷却(L187)
  • Sources/RDReaderView/EPUBUI/ReaderController/RDEPUBTextStatisticsCoordinator.swift:独立渲染 + 独立缓存
  • Sources/RDReaderView/EPUBUI/ReaderController/ChapterRuntime/RDEPUBChapterLoader.swift
    • loadChapterSynchronouslyForMigration:主线程信号量同步等待(L182)
  • Sources/RDReaderView/EPUBUI/RDEPUBReaderController+DataSource.swift
    • textProgressFooterText:百分比消费点(L142~153)
  • Sources/RDReaderView/EPUBUI/ReaderController/ChapterRuntime/RDEPUBChapterRuntimeStore.swift:chapterLoadQueue、pending load 状态

2. 关键设计事实与约束

方案的取舍建立在以下三个事实上,评审时先对齐。

2.1 一个百分比数字由两条流水线各出一半

Footer 百分比的计算是 statistics.percent(spineIndex:, chapterOffset:),两个输入来源不同:

  • 分子(当前位置):page.contentRange.location + length,即当前页在本章排版后字符串中的结束偏移——由章节运行时(交互加载/预取)的分页产出;
  • 分母(全书总字符数)与本章绝对起点 absoluteCharacterStart:由统计流水线产出。

两者必须在同一坐标系:DTCoreText 渲染后 attributedString 的字符长度。这带来一条硬约束:精确统计不能用"HTML 去标签的文本长度"替代(坐标系与分子不一致);近似值只允许用于分母侧的估计(见阶段 4),分子永远来自真实分页。

2.2 预加载不可被统计扫描替代

两边虽然做同样的渲染动作,产物和时限完全不同:

  • 预加载的产物是驻留内存的可绘制对象(RDEPUBRuntimeChapter:排版串 + layouter + 分页),翻页瞬间直接取用;统计的产物只是一个数字,昂贵的排版结果随 autoreleasepool 丢弃。
  • summary 缓存命中仍需一次完整 DTCoreText 渲染才能显示(buildChapterFromCachedPageRanges 只免分页、不免渲染)。真正保证翻页零等待的是 prefetch 把 RuntimeChapter 提前放进内存,这一角色无可替代。
  • 预加载是带截止时间的需求预测(用户 30 秒后会翻到下一章);统计是无截止时间的全量扫描。

因此去重方向是统计不再渲染(消费预加载产物 + 补渲染远端章节),而不是砍掉预加载。

2.3 结构性瓶颈:分母需要全书每章齐全

百分比的分母是全书字符总和,一章不齐数字就出不来——"footer 从'计算中'变成百分比"的时刻恒等于全书扫描完成的时刻,与谁来渲染、开几个线程无关(提速只能压缩这个时刻,不能消除)。要突破这个语义,唯一路径是近似分母先行(阶段 4)。


3. 优化目标

  1. 全书每章最多渲染一次(同一 renderSignature 下)。
  2. 交互(翻页/跳转)绝对优先:任何后台渲染在交互加载进行期间让路,让路机制为事件驱动而非时间窗轮询。
  3. 阅读百分比首次可用时间压到亚秒级(近似值),精确值完成时间显著优于现状(消除重复渲染后后台总量减半)。
  4. 首屏时间不劣化,目标改善(去掉首屏路径上的非必要串行工作)。

4. 方案总览

将"三条流水线"收敛为"一条交互流水线 + 一条后台生产流水线":

交互流水线(不变,最高优先级,不受后台预算约束)
  RDEPUBChapterLoader / chapterLoadQueue (userInitiated)
  navigation / preview / prefetch
  渲染完成 → RuntimeChapter + Summary 写盘
            └──(新增)characterCount 上报统计聚合器

后台生产流水线(由统计流水线改造而来)
  QoS .utility / 并发自适应(1~预算上限) / 章间事件驱动让路
  排序:距当前阅读位置由近到远(复用 backgroundPriorityManager)
  每章:Summary 缓存命中 → 直接取字符数(零渲染)
        miss → 渲染 + 分页 → 写 Summary + 上报字符数
  产出:① 全书 PageMap 增量收敛(总页数越来越准)
        ② 全书字符统计(阅读百分比,近似值先行、精确值收敛)

共享组件
  交互加载门(5.6):navigation/preview 加载期间后台全体暂停
  全局后台渲染预算池(5.7):所有后台渲染共享并发上限
  后台渲染登记表(5.8):后台流水线间的飞行中去重

核心思想:统计流水线不再是"为了一个数字渲染全书"的消费者,而是升级为架构中缺失的"全书 summary 生产者"——一次渲染两个产品(翻页要分页结果,统计要字符数)。chapterLoader 后续翻页大概率命中 summary 缓存,翻页反而更快。

MetadataParseWorker 的窗口覆盖职责与新生产者重叠,分两步处理(见阶段 2):先治理(接入预算池与让路门 + init 出主线程),后评估合并移除。


5. 分阶段实施

5.1 阶段 1:统计流水线 → 全书章节索引生产者(核心)

改造 RDEPUBTextStatisticsCoordinator / RDEPUBTextStatisticsWorker(建议更名为 RDEPUBBookIndexingCoordinator / Worker,体现双职责):

  1. 读 summary 缓存优先:每章先查 RDEPUBChapterSummaryDiskCache(key 一致)。命中时字符数 = 末段 pageRange 的 location + length,零渲染。不新增 summary 字段、不动 schemaVersion。边界兜底:该等式依赖"分页覆盖全文";读取时校验 pageRanges 非空、末段 end 单调且 pageCount == pageRanges.count(sanitizedPageRanges 的存在说明缓存出现过脏 range),校验不过视为 miss 重渲染,并加 debug 断言。
  2. miss 时渲染一次、产出两份:渲染(传入真实 pageSize,当前统计传 nil)+ 分页,写 RDEPUBChapterSummary(复用 RDEPUBChapterLoader.makeSummary 的构造逻辑,抽成共享工具),同时上报字符数。CFI map 沿用"延迟构建"策略,不在生产者内做。
  3. 渲染顺序按阅读位置:废弃 spine 顺序,使用 backgroundPriorityManager.makeMetadataPriorityOrder(现成:当前章优先、warm anchor 次之、cold 游标兜底)。
  4. 章间让路 + 飞行中去重:每章开始前先过交互加载门(5.6),再向后台渲染登记表(5.8)登记本章;登记失败(他人在渲染)则跳过取下一章。取消(cancellationController)可随时打断等待。
  5. PageMap 增量喂给主线程:每产出 N 章(复用 pageMapRefreshInterval = 32 或取更小值)将累计 summary 合并为 partial map,走现有 refreshBookPageMapInPlace 路径。
  6. 接收交互流水线的产出:RDEPUBChapterLoader 渲染完成回调中把 (spineIndex, characterCount) 推给统计聚合器——用户翻页产生的渲染同样贡献统计,生产者跳过这些章;用户读得越活跃,统计越快。
  7. 启动时机后移:从 paginatePublication 开头移到首屏 applyBookPageMap 完成之后(与 prefetch、metadata worker 同一批,但受让路门约束)。
  8. 移除 RDEPUBTextStatisticsDiskCache:summary 缓存成为唯一数据源。发布前保留一个版本的只读兼容(读旧统计缓存作为 fallback)可选,建议直接移除以降低复杂度。

聚合器在阶段 1 保持"凑齐才发布"语义(与现状等价,footer 显示"计算中");增量/近似发布在阶段 4 引入。

5.2 阶段 2:MetadataParseWorker 治理(先治理,后合并)

2a. 立即治理(低风险)

  1. 并发接入全局预算池(5.7),不再自行按全核计算;RDEPUBReaderConfiguration.metadataParsingConcurrency 语义改为"预算上限的用户侧覆盖",默认跟随预算池。
  2. init 中的 contentHashBySpineIndex 全量读文件 + sha256 移出主线程:移入 start() 的后台块,或 init 改为在调用方后台线程构造。
  3. 章间让路 + 飞行中去重:每个 chapter operation 执行块开头接入交互加载门(5.6)与后台渲染登记表(5.8),替代现有"仅启动时等 0.8 秒"的一次性冷却。共存期(阶段 2b 之前)生产者与 metadata worker 的目标章节高度重叠(都从 anchor 附近开始),登记表是保证"每章只渲染一次"的必要条件,不可省略。

2b. 后续合并(独立评审)

阶段 1 落地后,MetadataParseWorker 的价值收缩为"打开时快速凑齐 anchor 窗口的 PageMap"。评估将其移除,窗口覆盖由生产者的优先级排序天然承担(anchor 窗口排最前)。涉及 backgroundCoverageStorerestoreBookPageMapIfPossible、retry 逻辑的迁移,单独出 RFC,不与阶段 1 捆绑。

5.3 阶段 3:首屏路径优化(独立可做)

  1. loadInitialInteractiveRuntimeChapters 中相邻章的同步补载改为异步:首屏只等 anchor 章;anchor 章不足 2 页时先出首屏,相邻章以 .preview 优先级异步补,完成后走现有 queueForwardAppendedPageMap 扩展。
  2. paginateTextPublication 的编排队列由 .utility 提升为 .userInitiated(首屏关键路径)。

有意的 trade-off(需评审确认):现状同步补载的目的正是保证首屏出来时至少 2 页可翻。改异步后存在一个短暂窗口——单页首章 + 用户立刻翻页 → 命中 loading 页。缓解:补载走 .preview 优先级(userInitiated 队列,通常数百毫秒内就绪)、loading 页 footer 已有"计算中"占位处理;换来的是所有多页首章场景的首屏时间稳定改善。若评审认为该窗口不可接受,退化方案为"仅当 anchor 章 ≥ 2 页时走纯异步"。

5.4 阶段 4:两阶段近似百分比(正式纳入)

解决 2.3 的结构性瓶颈:footer 首个百分比时刻从"全书渲染扫完"提前到打开书后亚秒级。

  1. 快速近似扫描:打开书后在后台对全书每章做"HTML 去标签纯文本长度"统计(轻量 XML/正则 strip,不走渲染管线),全书量级为几十~几百毫秒,产出近似分母与近似 absoluteCharacterStart
  2. 混合分母收敛:聚合器为每章维护 精确值 | 近似值 标记;分母 = 精确值之和 + 未精确章节的近似值之和。精确值(来自 summary 命中、交互渲染上报、生产者渲染)逐章替换近似值,全部精确后与阶段 1 语义完全一致。
  3. 坐标系约束(见 2.1):近似值只进入分母与前序章节的起点估计;分子 chapterOffset 永远来自当前章真实分页(当前章正在显示,必然已渲染,其精确字符数同时可用)。误差来源仅剩前序章节近似与总量近似,典型偏差 <1% 且随阅读单调收敛(误差分级标准见第 8-3 节)。
  4. UI 语义:近似分母就绪即显示百分比,不再显示"计算中"(近似/精确不做视觉区分);"计算中"仅保留给近似扫描也未完成的极短窗口与 PageMap 未覆盖的 loading 页。
  5. 实现要点:RDEPUBBookTextStatistics 由一次性不可变构建改为支持逐章替换的聚合模型(内部仍可每次替换后重建不可变快照发布,避免并发可变状态);主线程刷新节流见 5.5-4。
  6. 持久化:近似值不落盘(计算便宜,冷启动重算);summary/精确值沿用磁盘缓存。

5.5 阶段 5:自适应并发渲染(默认启用)

前提:阶段 1(去重)已落地——否则多线程只是把重复劳动乘以 N。

  1. 并发预算:来自全局预算池(5.7),上限 min(3, max(1, activeProcessorCount - 2))
  2. 自适应降速(在"取下一章"调度点做准入,不动态改 OperationQueue 配置):
    • 活跃翻页期(secondsSinceLastUserNavigation() 小于阈值,建议 2 秒,与 LocationCoordinator 的 idle 判定一致)→ 收缩为 1;
    • ProcessInfo.thermalState >= .serious 或低电量模式 → 收缩为 1;
    • 收到内存警告 → 收缩为 1,并配合现有 evict;
    • 静止阅读/空闲 → 放开至预算上限。
  3. QoS 保持 .utility(P+E 核架构下低 QoS 线程主要落能效核,与主线程/chapterLoadQueue 的性能核天然隔离;不选 .background 以免 I/O 被系统节流拖长扫描)。
  4. UI 刷新节流:多线程下章节乱序完成(聚合已是加锁模型,天然支持);主线程刷新(footer/reloadPageCountOnly)合并为"每完成 8~16 章或每 0.5 秒"一次,禁止每章一刷。
  5. 收益预期:DTCoreText 存在 libxml/字体缓存等进程级锁,3 线程实际加速 2.2~2.5 倍,继续加线程边际收益趋零——这是预算上限取 3 的依据之一。

5.6 共享组件:交互加载门

新增 RDEPUBInteractiveLoadGate(NSCondition 封装),所有后台渲染流水线共用:

  • raise:RDEPUBChapterLoader 在 navigation / preview 优先级的 pending load 注册时计数 +1(prefetch 不计,prefetch 本身就是后台工作)。
  • lower:对应 load resolve(成功/失败)时计数 -1,归零时 broadcast 唤醒全部等待者。
  • wait:后台流水线章间调用 waitUntilClear(cancellation:);cancellation 触发时立即返回。
  • 只在章间边界等待,不打断进行中的单章渲染(单章通常数百毫秒,可接受;避免渲染中途状态管理复杂度)。多线程下语义不变:翻页瞬间后台从 N 并发降到 0,翻完集体恢复。

该门是此前已移除的"0.8 秒翻页时间窗轮询"的正确形态:无时间参数、无轮询、翻页结束即恢复。

5.7 共享组件:全局后台渲染预算池

动机:现状各流水线各自决定并发(metadata worker 按全核、统计 1、chapterLoadQueue 1),局部合理、总和超载。并发决策必须收敛到一处。

  • 形式:计数信号量,后台渲染总并发 = min(3, max(1, activeProcessorCount - 2))。所有后台流水线(生产者、metadata worker)每次章节渲染前取票、完成归还;交互流水线(navigation/preview)不受预算约束,永远直接执行
  • 为什么留 2 核而不是 1 核:翻页关键路径上同时有两条热线程——主线程(手势/转场/当前页绘制)与 chapterLoadQueue(未缓存章节的那次渲染,主线程可能正同步等它,其完成时间即翻页延迟)。只留 1 核时两者在最坏时刻互挤,卡顿复现。
  • "留核"的真实含义:iOS 无核绑定 API,可控的只有"后台线程数 × QoS";预算池控制线程数,QoS 控制调度倾向,内存带宽与进程级锁的共享是并发上限取 3 的另一依据。
  • 与交互加载门正交:门管"交互期间全体暂停",池管"平时最多同时渲染几章"。

5.8 共享组件:后台渲染登记表(飞行中去重)

summary 缓存只能去重"已完成"的渲染;同一章同时在两条后台流水线中飞行时(谁都还没写盘),缓存查询拦不住双渲染。共存期(阶段 2b 前)生产者与 metadata worker 的目标章节高度重叠,该场景必然发生,不是边角情况。

  • 形式:全局 Set<spineIndex> + 锁(参照现有 beginPendingChapterLoad 模式),按 renderSignature 命名空间隔离。
  • 规则:后台流水线(生产者、metadata worker)取章渲染前 begin(spineIndex:) 登记,失败(他人在渲染)即跳过取下一章;完成/失败/取消时注销。
  • 交互流水线不受约束:navigation/preview 加载不查表、不等表——让 userInitiated 的翻页渲染去等 utility 的后台渲染是优先级反转。交互与后台撞车导致的偶发双渲染予以接受,记 duplicateRender 日志计数(纳入第 8-1 节验证)。
  • 生产者取章时同时检查 store.chapterData / hasPendingChapterLoad,已被交互体系覆盖的章节直接等其上报,不自行渲染。

6. 性能预期(按场景)

场景 现状 方案后 依据
冷启动大书,footer 首个百分比 全书渲染扫完(几十秒~分钟级) 亚秒级(近似值),精确值后台收敛 阶段 4
冷启动大书,精确统计完成时间 单线程 × 与 metadata worker 全程抢核 持平或更快:去重(窗口章节免渲染)+ 无重复流水线竞争 + 空闲期 2~3 并发;单章成本 +约 30%(增加分页)被上述收益覆盖 阶段 1 + 5
二次打开 / 读过一部分 统计缓存独立,预加载渲染过的章节仍是 miss,需重渲染 快一个量级:读过/预加载过的章节直接读 summary(毫秒级/章) 阶段 1
统计期间翻页 主线程同步渲染被后台放大 2~3 倍 后台交互期间为 0 并发(让路门),平时不超预算;翻页目标章大概率 summary 命中(免分页) 5.6 + 5.7
让路对统计的代价 总完成时间仅增加"用户实际交互占用的时长",无空转等待 5.6

7. 风险与对策

风险 对策
布局变化(转屏、字号)导致 renderSignature 失效,生产者产出作废 沿用现有 token + cancellationController 机制:设置变更时 cancel 并以新 snapshot 重启;summary 缓存按 signature 命名空间隔离,旧产出不污染。近似分母与 renderSignature 无关,无需重算
生产者渲染中用户跳转,让路门只拦章间 单章渲染粒度可接受;跳转走 navigation 优先级,chapterLoadQueue(userInitiated)调度上压过生产者(utility),且交互不受预算池约束
让路门计数泄漏导致后台永久挂起 raise/lower 严格配对在 registerPendingLoad / resolvePendingLoad(已有单点出入口);增加 debug 断言 + 兜底超时日志(不自动放行,只报警)
预算池取票/归还不配对导致后台饿死 取票/归还封装为 scoped helper(defer 归还);debug 断言校验余额
生产者写 summary 覆盖 loader 已写入的更完整 summary(loader 侧经延迟构建后含 cfiMap,生产者侧不含,后写会把 cfiMap 抹掉导致重复构建) 生产者写盘前 re-check:该 key 已存在 summary 即跳过写入(只取字符数);写路径本身沿用 RDEPUBChapterSummaryDiskCache 串行队列 + 原子替换。配合 5.8 登记表,正常情况下不会走到同章双写
近似百分比与精确值偏差被用户感知 近似仅用于分母(2.1 坐标系约束),偏差单调收敛;分级标准与抽样验证见第 8-3 节
PageMap 增量刷新引起当前页码跳动 复用现有 refreshBookPageMapInPlace / reconciliation 路径,不新增刷新语义;刷新间隔沿用 32 章
多线程渲染内存峰值(每 worker 持有整章排版串 + DTCoreText 中间 DOM) 每章 autoreleasepool;不持有 RuntimeChapter,写盘后即释放;内存警告时并发收缩为 1;3 并发峰值约为单线程 3 倍,预算上限已封顶
多线程撞 DTCoreText/CoreText 进程级锁,加速比不达预期 预期管理:2.2~2.5 倍为目标;若实测低于 1.5 倍,回退默认并发 1(自适应策略保留,阈值可配)

8. 验收与度量

上线前后对比(同一测试书目,冷缓存 / 热缓存各一组):

  1. 每章渲染次数 ≤ 1:扩展 [TextStatistics] 日志为来源分布 summaryCacheHit / rendered / fromInteractiveLoad / approximated,全书求和验证。
  2. footer 首个百分比出现时间:目标亚秒级(近似值);同时记录精确值收敛完成时间。
  3. 近似误差(统一分级标准):样本书目(不同语言、图文比例、章节数量级)上对比近似百分比与最终精确百分比——典型值预期 <1%;验收线为偏差分布 P95 < 1.5%;任一单本书 > 3% 触发个案分析(定位该书目类型的系统性偏差来源,如实体密集、大量脚注)。
  4. 翻页流畅度:loadChapterSynchronouslyForMigration 耗时日志 P50/P95;统计计算期间连续翻页 20 次的主线程 hang 时长(Instruments Hangs)。
  5. 统计完成时间:lastTextStatisticsWallClockMs,目标冷缓存下降 ≥ 40%(消除重复渲染 + CPU 竞争;空闲期自适应并发另计)。
  6. 多线程加速比:空闲场景 1 / 2 / 3 并发的完成时间对比,验证 ≥ 2 倍再默认放开;同时记录热状态触发降档的频率。
  7. 首屏时间(阶段 3):打开到首帧渲染完成,目标不劣化、多页首章场景改善。
  8. PageMap 收敛速度:总页数从"计算中"到稳定值的耗时,目标不劣于现状(生产者兼职覆盖全书,应优于现状)。

9. 实施顺序建议

  1. 交互加载门(5.6)+ 预算池(5.7)+ 登记表(5.8)+ 阶段 2a(治理 metadata worker)——小改动,立即缓解翻页卡顿。
  2. 阶段 1(生产者化)——核心重构,消除重复渲染。
  3. 阶段 4(两阶段近似百分比)——直接兑现"首个百分比亚秒级"的产品收益;依赖阶段 1 的聚合器改造,不依赖阶段 5。
  4. 阶段 3(首屏)——独立优化,可与 3 并行。
  5. 阶段 5(自适应并发,默认启用)——依赖阶段 1 去重与 5.7 预算池;按第 8-6 节实测加速比决定是否默认放开到 3。
  6. 阶段 2b(合并 metadata worker)——后续 RFC。