长章节内存优化 P0:每页子串改按需构造并新增内存打点
- RDEPUBTextPage.content 由存储属性改为基于 chapterContent+contentRange 的计算属性,三处构造点不再生成每页子串,章节缓存窗口内每章少一份 整章文本常驻副本 - 新增 RDEPUBMemoryProbe(--demo-memory-probe 开启),在章节插入、 每 20 次翻页、设置失效、转屏完成时输出 phys_footprint - shouldAvoidReaderPageCaching 补注释,锁定放开前置条件为 P1-1 - 新增实施清单文档 LONG_CHAPTER_MEMORY_OPTIMIZATION_PLAN.md 并勾选 P0 验证:Demo 编译通过;UI 回归 9/9 通过(PageNavigation/ ReaderOpenClose/LargeBookOnDemand) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
5a41066b66
commit
e4e629a06c
156
Doc/LONG_CHAPTER_MEMORY_OPTIMIZATION_PLAN.md
Normal file
156
Doc/LONG_CHAPTER_MEMORY_OPTIMIZATION_PLAN.md
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
# 长章节内存优化 — 代码实施清单
|
||||||
|
|
||||||
|
> 背景:DTCoreText 文本页已采用"整章上下文布局",分页与显示边界一致(不可回退)。现状代价:每次翻页/预加载都整章重拷贝 + 重排版后丢弃,且章节缓存里每页常驻一份子串副本。
|
||||||
|
>
|
||||||
|
> 总原则:保留"整章参与排版",去掉"整章按页复制、按页可变、按页缓存"。不改分页器,不上窗口化。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## P0:立即可做
|
||||||
|
|
||||||
|
### P0-1 削减每页 `content` 子串常驻副本
|
||||||
|
|
||||||
|
**现状**:分页产物为每页保存整段 `attributedSubstring`,全章加总约等于又一份整章副本,随邻接窗口(radius 1,共 3 章)常驻。
|
||||||
|
|
||||||
|
生成点:
|
||||||
|
|
||||||
|
- `Sources/RDReaderView/EPUBUI/ReaderController/ChapterRuntime/RDEPUBChapterLoader.swift:494`(`buildPagesFromRanges`)
|
||||||
|
- `Sources/RDReaderView/EPUBTextRendering/BuildPipeline/RDEPUBTextBookBuilder.swift:294`
|
||||||
|
- `Sources/RDReaderView/EPUBTextRendering/RDPlainTextBookBuilder.swift:78`
|
||||||
|
|
||||||
|
现存全部消费点(已盘点确认,仅 3 处):
|
||||||
|
|
||||||
|
- `RDEPUBTextContentView.swift:562` — 封面检测 `coverImage(from: page.content)`,仅 `pageIndexInChapter == 0` 且 href 含 "cover"
|
||||||
|
- `RDEPUBTextContentView.swift:641` — `normalizedPageContent`,仅非 DTCoreText 的 `#else` 回退路径调用
|
||||||
|
- `RDEPUBTextBookBuilder.swift:93` — 构建期 debug 日志 preview
|
||||||
|
|
||||||
|
**任务**:
|
||||||
|
|
||||||
|
- [x] 将 `RDEPUBTextPage.content` 改为按需构造:已改为基于 `chapterContent` + `contentRange` 的计算属性(带范围钳制),公开读取 API 不变
|
||||||
|
- [x] 三个构造点(`RDEPUBChapterLoader` / `RDEPUBTextBookBuilder` / `RDPlainTextBookBuilder`)不再生成每页子串;消费点经计算属性透明按需构造
|
||||||
|
- [x] `Equatable` 确认:`content` 为派生值,由 `contentRange` + `chapterContent` 判等覆盖,语义不变
|
||||||
|
|
||||||
|
**API 兼容性注意**:
|
||||||
|
|
||||||
|
- `RDEPUBTextPage` 当前是 `public struct`,`content` 也是公开存储属性;如果 SDK 已存在外部接入方直接读取 `page.content`,则不应直接移除该字段
|
||||||
|
- 优先方案是保留 `content` 对外访问语义,但将其改为按需构造的计算属性或受控访问器,避免对外 API 断裂
|
||||||
|
- 若确认当前版本尚无稳定外部依赖,才可以评估把 `content` 从存储属性调整为内部实现细节
|
||||||
|
- 在实施前应补做一次全仓库与接入侧检索,确认是否有外部调用依赖 `page.content` 的“可变存储”特性
|
||||||
|
|
||||||
|
**验收**:打开 3 章窗口后常驻内存下降约"一整章文本规模 × 窗口章数";封面页显示正常;非 DTCoreText 回退路径回归通过;`--demo-pagination-validate` 命中数不回升。
|
||||||
|
|
||||||
|
### P0-2 增加内存打点
|
||||||
|
|
||||||
|
- [x] 已新增 `RDEPUBMemoryProbe`(`--demo-memory-probe` 启动参数开启,输出 phys_footprint);挂载点:`RDEPUBChapterRuntimeStore.insertChapter`、`pageNum` 回调每 20 次翻页、`invalidateAllForSettingsChange`、`viewWillTransition` 转屏完成
|
||||||
|
- [x] 用 `--demo-memory-probe` 运行 Demo 即可采集对比基线
|
||||||
|
|
||||||
|
### P0-3 保持现有缓存限制,防止错误复用
|
||||||
|
|
||||||
|
- [x] `shouldAvoidReaderPageCaching`(`RDEPUBTextContentView.swift:319`)保持现状,已补注释说明放开的前置条件是 P1-1 落地
|
||||||
|
- [x] 本阶段未改 `RDReaderPreloadController` 行为
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## P1:第二波(P1-1 与 P1-2 必须同批落地)
|
||||||
|
|
||||||
|
### P1-1 章节级共享 displayContent
|
||||||
|
|
||||||
|
**现状**:`RDEPUBTextContentView.configure` 每次创建整章可变副本(`RDEPUBTextContentView.swift:443`)并按页写入三类属性:页范围主题色(453)、页范围暗黑图附件替换(448)、按页裁剪的高亮属性(459 `applyHighlightsToContent`)。
|
||||||
|
|
||||||
|
**约束**:共享字符串一旦被任一页的 `DTCoreTextLayouter`/framesetter 持有,就不得再修改——所有属性必须在共享内容构建时一次性注入,或转为 overlay(见 P1-2)。
|
||||||
|
|
||||||
|
**任务**:
|
||||||
|
|
||||||
|
- [ ] 在章节运行时侧(建议挂在 `RDEPUBRuntimeChapter` 或新建 `RDEPUBChapterDisplayCache`)新增:
|
||||||
|
- `chapterDisplayContent`(`typesetAttributedString` 的一次性拷贝)
|
||||||
|
- `displaySignature`(组成:主题、暗黑图配置、字体设置、本章高亮集合版本)
|
||||||
|
- [ ] 构建共享内容时一次性注入:全章范围主题色;全章范围高亮/下划线属性(若 P1-2 决定转 overlay 则此项取消)
|
||||||
|
- [ ] 确定暗黑图策略(二选一):
|
||||||
|
- a. 构建时全章一次性替换(依赖 `RDEPUBDarkImageAdjuster` 的 NSCache 兜底解码成本)
|
||||||
|
- b. 移到渲染时按可见附件替换,不触碰共享字符串
|
||||||
|
- [ ] `RDEPUBTextContentView.configure` 改为接收"共享内容引用 + `page.contentRange`",删除 443 行的整章拷贝
|
||||||
|
- [ ] 高亮增删、搜索激活、主题/设置变化 → 比对 `displaySignature`,不一致则重建共享内容(禁止原地修改)
|
||||||
|
- [ ] 章节淘汰(`RDEPUBChapterRuntimeStore.evict` / `handleMemoryWarning` / `invalidateAllForSettingsChange`)同步释放 display cache
|
||||||
|
|
||||||
|
**验收**:连续翻页不再出现整章拷贝(打点确认);翻页延迟下降;高亮显示、高亮点击菜单、underline、搜索 currentMatch、附件点击、选区拖拽全部回归通过。
|
||||||
|
|
||||||
|
### P1-2 高亮/下划线剥离为 overlay(与 P1-1 绑定)
|
||||||
|
|
||||||
|
**现状**:搜索高亮已走 overlay(`RDEPUBTextContentView.swift:759` 处 `buildDecorations` 传 `highlights: []`,注释明确为避免双画);普通高亮/下划线仍靠写 `kRDEPUBHighlightAttributeName` / `kRDEPUBUnderlineAttributeName` 属性由 render view 绘制。
|
||||||
|
|
||||||
|
**任务**:
|
||||||
|
|
||||||
|
- [ ] `buildDecorations` 改为传入真实 highlights,高亮/下划线由 decoration 层绘制
|
||||||
|
- [ ] 移除 `applyHighlightsToContent`(`RDEPUBTextContentView.swift:617`)及 render view 对两个自定义属性的绘制路径,确认无双画
|
||||||
|
- [ ] 高亮命中(`highlight(at:)`)与菜单锚点继续走 `interactionController.selectionRects`,坐标保持 chapter-absolute
|
||||||
|
|
||||||
|
**验收**:高亮/下划线视觉与改前一致(含跨页高亮的页内裁剪);高亮点击弹菜单正常;与搜索高亮叠加时无双画。
|
||||||
|
|
||||||
|
### P1-3 章节级共享 display layouter
|
||||||
|
|
||||||
|
**现状**:每个页面视图各建一个 `DTCoreTextLayouter`(`RDEPUBTextContentView.swift:465`);章节运行时已有分页用 `RDEPUBTextLayouter`(`RDEPUBRuntimeChapter.layouter`),display layouter 向同一模式靠拢。
|
||||||
|
|
||||||
|
**任务**:
|
||||||
|
|
||||||
|
- [ ] `DTCoreTextLayouter` 随 `chapterDisplayContent` 一起放入章节级 display cache,与 signature 同生命周期
|
||||||
|
- [ ] 页面按 `bounds` + `page.contentRange` 取 `layoutFrame`;`RDEPUBTextPageVerticalJustifier.justify` 会修改 layoutFrame,因此 **保持 `shouldCacheLayoutFrames = false`**,layoutFrame 永远按页新建、不共享
|
||||||
|
- [ ] bounds 变化(横竖屏/双页)纳入失效条件;确认旧 layoutFrame 随页面视图释放
|
||||||
|
- [ ] `RDEPUBTextPageBoundaryValidator` 校验路径不变
|
||||||
|
|
||||||
|
**验收**:翻页时不再重建 framesetter(打点确认);横竖屏切换后布局正确;`--demo-pagination-validate` 不回升。
|
||||||
|
|
||||||
|
### P1-4 评估重新放开文本页预加载缓存
|
||||||
|
|
||||||
|
前置:P1-1~P1-3 全部落地后,页面视图不再持有整章副本。
|
||||||
|
|
||||||
|
- [ ] 调整 `shouldAvoidReaderPageCaching` 判定,允许轻量化后的文本页进入 `RDReaderPreloadController` 缓存
|
||||||
|
- [ ] 对比放开前后的翻页流畅度与峰值内存,数据不佳则回退此项
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## P2:视 P0/P1 打点数据决定
|
||||||
|
|
||||||
|
### P2-1 章节级 LRU 分层淘汰
|
||||||
|
|
||||||
|
**现状**:`RDEPUBChapterRuntimeStore` 已有邻接窗口(`windowSpineIndices`,radius 1)、`evictableSpineIndices` / `evict` / `evictAllExceptCurrent` / `handleMemoryWarning` / `invalidateAllForSettingsChange`;磁盘侧有 `RDEPUBChapterSummaryDiskCache`(分页结果 + cfiMap + metadata)。当前邻接章节仍持有完整 `RDEPUBRuntimeChapter`(typesetString + pages + layouter)。
|
||||||
|
|
||||||
|
**任务**:
|
||||||
|
|
||||||
|
- [ ] 邻接章节降级:保留 page map / chapter summary / 分页结果,释放 `typesetAttributedString`、display content、layouter
|
||||||
|
- [ ] 进入邻接章节时按 summary 重建,测量重建延迟;延迟不可接受则维持现状
|
||||||
|
- [ ] 远距章节仅保留磁盘缓存 + 轻量 metadata(现有 evict 已覆盖,确认即可)
|
||||||
|
|
||||||
|
### P2-2 图片附件深化
|
||||||
|
|
||||||
|
**现状**:暗黑图已走 `NSCache`(50 MB / 100 张,`RDEPUBDarkImageAdjuster.swift:9`)且仅处理当前页范围;章节运行时另有 100 MB `imageCache`。
|
||||||
|
|
||||||
|
**任务**:
|
||||||
|
|
||||||
|
- [ ] 大图按显示尺寸下采样后再参与 attachment
|
||||||
|
- [ ] 页面离屏后释放 attachment 强引用的已解码大图;避免共享 content 长期持有
|
||||||
|
- [ ] 回归:图片点击查看、暗黑模式切换、附件位置与命中
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## P3:仅在 P0–P2 之后数据仍不达标时
|
||||||
|
|
||||||
|
- [ ] 上下文窗口化布局(当前页所在段落块 + 前后足以影响断行的文本窗口)。风险最高:易重新引入分页/显示边界不一致(CTTypesetter 断行上下文敏感,为已知已解 bug 的根因),选区/搜索/高亮/附件索引需全部重验。默认不做。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 明确不做
|
||||||
|
|
||||||
|
1. 回退"页内子串重新布局"(重新引入显示不全/页末孤字)
|
||||||
|
2. 取消 chapter-absolute 索引(破坏点击/选区/高亮定位一致性)
|
||||||
|
3. 先改分页器(正确性与性能问题混杂,无法回归)
|
||||||
|
4. 未落地 P1 前放开文本页 reader 级缓存(会驻留多份整章副本)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 各阶段统一验收指标
|
||||||
|
|
||||||
|
**正确性**:`--demo-pagination-validate` 命中数不回升;长章节页末无孤字/缺字;高亮显示、搜索高亮、附件点击、选区拖拽行为一致。
|
||||||
|
|
||||||
|
**内存**(对比 P0-2 基线):打开长章节后常驻;连续翻页 20 页峰值;设置变更前后峰值;横竖屏切换峰值。
|
||||||
|
|
||||||
|
**性能**:首次打开章节耗时;连续翻页帧稳定性;设置切换恢复时间。
|
||||||
@ -38,6 +38,7 @@
|
|||||||
| [CFI_SUBSYSTEM.md](CFI_SUBSYSTEM.md) | CFI 子系统详解:EPUB CFI 解析、生成、序列化、范围、恢复引擎 |
|
| [CFI_SUBSYSTEM.md](CFI_SUBSYSTEM.md) | CFI 子系统详解:EPUB CFI 解析、生成、序列化、范围、恢复引擎 |
|
||||||
| [CFI_ISSUES_REVIEW.md](CFI_ISSUES_REVIEW.md) | CFI 实现问题分析报告:11 个问题(3 高 / 5 中 / 3 低),含修复优先级建议 |
|
| [CFI_ISSUES_REVIEW.md](CFI_ISSUES_REVIEW.md) | CFI 实现问题分析报告:11 个问题(3 高 / 5 中 / 3 低),含修复优先级建议 |
|
||||||
| [PAGINATION_ISSUES_2026-07-06.md](PAGINATION_ISSUES_2026-07-06.md) | 分页问题调查记录:脚注图标 avoid-trim 裁行(已修复)+页范围/显示度量错配行中断页(待复现),含调试命令备忘 |
|
| [PAGINATION_ISSUES_2026-07-06.md](PAGINATION_ISSUES_2026-07-06.md) | 分页问题调查记录:脚注图标 avoid-trim 裁行(已修复)+页范围/显示度量错配行中断页(待复现),含调试命令备忘 |
|
||||||
|
| [LONG_CHAPTER_MEMORY_OPTIMIZATION_PLAN.md](LONG_CHAPTER_MEMORY_OPTIMIZATION_PLAN.md) | 长章节内存优化低风险实施方案:在保留整章上下文布局正确性的前提下,分阶段收敛整章副本、共享 layouter、治理缓存与图片内存 |
|
||||||
| [API_REFERENCE.md](API_REFERENCE.md) | 公开 API 参考:RDEPUBReaderController 公开接口、委托协议、配置模型 |
|
| [API_REFERENCE.md](API_REFERENCE.md) | 公开 API 参考:RDEPUBReaderController 公开接口、委托协议、配置模型 |
|
||||||
| [当前阅读器问题修复开发清单.md](当前阅读器问题修复开发清单.md) | 基于当前代码实现整理的逐任务开发计划:10 个任务、修改位置、实施步骤、风险点与验收标准 |
|
| [当前阅读器问题修复开发清单.md](当前阅读器问题修复开发清单.md) | 基于当前代码实现整理的逐任务开发计划:10 个任务、修改位置、实施步骤、风险点与验收标准 |
|
||||||
| [大书远距目录跳转与后台补全优化方案.md](大书远距目录跳转与后台补全优化方案.md) | 面向大书按需分页模式的完整优化方案:远距目录跳转、后台补全优先级、页图接管协议与分阶段实施路径 |
|
| [大书远距目录跳转与后台补全优化方案.md](大书远距目录跳转与后台补全优化方案.md) | 面向大书按需分页模式的完整优化方案:远距目录跳转、后台补全优先级、页图接管协议与分阶段实施路径 |
|
||||||
|
|||||||
2510
ReadViewDemo/Pods/Pods.xcodeproj/project.pbxproj
generated
2510
ReadViewDemo/Pods/Pods.xcodeproj/project.pbxproj
generated
File diff suppressed because it is too large
Load Diff
@ -291,7 +291,6 @@ public final class RDEPUBTextBookBuilder {
|
|||||||
pageIndexInChapter: localPageIndex,
|
pageIndexInChapter: localPageIndex,
|
||||||
totalPagesInChapter: effectiveFrames.count,
|
totalPagesInChapter: effectiveFrames.count,
|
||||||
chapterContent: chapterAttributedContent,
|
chapterContent: chapterAttributedContent,
|
||||||
content: content.attributedSubstring(from: range),
|
|
||||||
contentRange: range,
|
contentRange: range,
|
||||||
pageStartOffset: range.location,
|
pageStartOffset: range.location,
|
||||||
pageEndOffset: range.location + max(range.length - 1, 0),
|
pageEndOffset: range.location + max(range.length - 1, 0),
|
||||||
|
|||||||
@ -54,7 +54,15 @@ public struct RDEPUBTextPage: Equatable {
|
|||||||
|
|
||||||
public var chapterContent: NSAttributedString
|
public var chapterContent: NSAttributedString
|
||||||
|
|
||||||
public var content: NSAttributedString
|
/// Page substring derived on demand from `chapterContent` + `contentRange`.
|
||||||
|
/// Not stored: keeping a per-page substring alive roughly doubles the
|
||||||
|
/// chapter's resident text memory across the chapter cache window.
|
||||||
|
public var content: NSAttributedString {
|
||||||
|
let bounds = NSRange(location: 0, length: chapterContent.length)
|
||||||
|
let clamped = NSIntersectionRange(contentRange, bounds)
|
||||||
|
guard clamped.length > 0 else { return NSAttributedString() }
|
||||||
|
return chapterContent.attributedSubstring(from: clamped)
|
||||||
|
}
|
||||||
|
|
||||||
public var contentRange: NSRange
|
public var contentRange: NSRange
|
||||||
|
|
||||||
|
|||||||
@ -75,7 +75,6 @@ public final class RDPlainTextBookBuilder {
|
|||||||
pageIndexInChapter: localPageIndex,
|
pageIndexInChapter: localPageIndex,
|
||||||
totalPagesInChapter: effectiveFrames.count,
|
totalPagesInChapter: effectiveFrames.count,
|
||||||
chapterContent: chapterAttributedContent,
|
chapterContent: chapterAttributedContent,
|
||||||
content: content.attributedSubstring(from: range),
|
|
||||||
contentRange: range,
|
contentRange: range,
|
||||||
pageStartOffset: range.location,
|
pageStartOffset: range.location,
|
||||||
pageEndOffset: range.location + max(range.length - 1, 0),
|
pageEndOffset: range.location + max(range.length - 1, 0),
|
||||||
|
|||||||
@ -345,6 +345,7 @@ extension RDEPUBReaderController: RDReaderDataSource, RDReaderPageProvider, RDRe
|
|||||||
}
|
}
|
||||||
|
|
||||||
public func pageNum(readerView: RDReaderView, pageNum: Int) {
|
public func pageNum(readerView: RDReaderView, pageNum: Int) {
|
||||||
|
RDEPUBMemoryProbe.logPageTurn()
|
||||||
readerContext.markUserNavigationActivity()
|
readerContext.markUserNavigationActivity()
|
||||||
updateCurrentSelection(nil)
|
updateCurrentSelection(nil)
|
||||||
reconcileTextPaginationSizeIfNeeded(for: pageNum)
|
reconcileTextPaginationSizeIfNeeded(for: pageNum)
|
||||||
|
|||||||
@ -297,6 +297,9 @@ public final class RDEPUBReaderController: UIViewController {
|
|||||||
public override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
|
public override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
|
||||||
super.viewWillTransition(to: size, with: coordinator)
|
super.viewWillTransition(to: size, with: coordinator)
|
||||||
runtime.viewportMonitor.viewWillTransition(with: coordinator)
|
runtime.viewportMonitor.viewWillTransition(with: coordinator)
|
||||||
|
coordinator.animate(alongsideTransition: nil) { _ in
|
||||||
|
RDEPUBMemoryProbe.log("orientationTransition size=\(Int(size.width))x\(Int(size.height))")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func showSearchBar() {
|
func showSearchBar() {
|
||||||
|
|||||||
@ -491,7 +491,6 @@ final class RDEPUBChapterLoader {
|
|||||||
) -> [RDEPUBTextPage] {
|
) -> [RDEPUBTextPage] {
|
||||||
let totalPageCount = pageRanges.count
|
let totalPageCount = pageRanges.count
|
||||||
return pageRanges.enumerated().map { (pageIndex, range) in
|
return pageRanges.enumerated().map { (pageIndex, range) in
|
||||||
let pageContent = typesetString.attributedSubstring(from: range)
|
|
||||||
let metadata: RDEPUBTextPageMetadata
|
let metadata: RDEPUBTextPageMetadata
|
||||||
if let metaList = metadataSource, pageIndex < metaList.count {
|
if let metaList = metadataSource, pageIndex < metaList.count {
|
||||||
|
|
||||||
@ -513,7 +512,6 @@ final class RDEPUBChapterLoader {
|
|||||||
pageIndexInChapter: pageIndex,
|
pageIndexInChapter: pageIndex,
|
||||||
totalPagesInChapter: totalPageCount,
|
totalPagesInChapter: totalPageCount,
|
||||||
chapterContent: typesetString,
|
chapterContent: typesetString,
|
||||||
content: pageContent,
|
|
||||||
contentRange: range,
|
contentRange: range,
|
||||||
pageStartOffset: range.location,
|
pageStartOffset: range.location,
|
||||||
pageEndOffset: range.location + range.length - 1,
|
pageEndOffset: range.location + range.length - 1,
|
||||||
|
|||||||
@ -62,6 +62,7 @@ final class RDEPUBChapterRuntimeStore {
|
|||||||
|
|
||||||
func insertChapter(_ chapter: RDEPUBRuntimeChapter) {
|
func insertChapter(_ chapter: RDEPUBRuntimeChapter) {
|
||||||
chapterDataCache[chapter.spineIndex] = chapter
|
chapterDataCache[chapter.spineIndex] = chapter
|
||||||
|
RDEPUBMemoryProbe.log("chapterLoaded spine=\(chapter.spineIndex) pages=\(chapter.pages.count)")
|
||||||
}
|
}
|
||||||
|
|
||||||
func insertPageCount(_ pc: RDEPUBRuntimePageCount, for key: RDEPUBChapterCacheKey) {
|
func insertPageCount(_ pc: RDEPUBRuntimePageCount, for key: RDEPUBChapterCacheKey) {
|
||||||
@ -188,6 +189,7 @@ final class RDEPUBChapterRuntimeStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func invalidateAllForSettingsChange() {
|
func invalidateAllForSettingsChange() {
|
||||||
|
RDEPUBMemoryProbe.log("settingsChangeInvalidateAll")
|
||||||
chapterDataCache.removeAll()
|
chapterDataCache.removeAll()
|
||||||
pageCountCache.removeAll()
|
pageCountCache.removeAll()
|
||||||
imageCache.removeAllObjects()
|
imageCache.removeAllObjects()
|
||||||
|
|||||||
@ -0,0 +1,43 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
/// Memory footprint probe for the long-chapter optimization work
|
||||||
|
/// (Doc/LONG_CHAPTER_MEMORY_OPTIMIZATION_PLAN.md, P0-2). Enabled with the
|
||||||
|
/// `--demo-memory-probe` launch argument; logs phys_footprint at chapter
|
||||||
|
/// load, every 20 page turns, settings invalidation, and rotation.
|
||||||
|
enum RDEPUBMemoryProbe {
|
||||||
|
|
||||||
|
static let isEnabled = ProcessInfo.processInfo.arguments.contains("--demo-memory-probe")
|
||||||
|
|
||||||
|
private static let pageTurnLogStride = 20
|
||||||
|
|
||||||
|
/// Main-thread only (page turns are delivered on main).
|
||||||
|
private static var pageTurnCount = 0
|
||||||
|
|
||||||
|
static func logPageTurn() {
|
||||||
|
guard isEnabled else { return }
|
||||||
|
pageTurnCount += 1
|
||||||
|
guard pageTurnCount % pageTurnLogStride == 0 else { return }
|
||||||
|
log("pageTurn count=\(pageTurnCount)")
|
||||||
|
}
|
||||||
|
|
||||||
|
static func log(_ event: String) {
|
||||||
|
guard isEnabled else { return }
|
||||||
|
let megabytes = Double(currentFootprint()) / 1_048_576
|
||||||
|
print(String(format: "[EPUB][MemoryProbe] %@ footprint=%.1fMB", event, megabytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// phys_footprint matches the value Xcode's memory gauge and Jetsam use.
|
||||||
|
private static func currentFootprint() -> UInt64 {
|
||||||
|
var info = task_vm_info_data_t()
|
||||||
|
var count = mach_msg_type_number_t(
|
||||||
|
MemoryLayout<task_vm_info_data_t>.size / MemoryLayout<integer_t>.size
|
||||||
|
)
|
||||||
|
let result = withUnsafeMutablePointer(to: &info) { pointer in
|
||||||
|
pointer.withMemoryRebound(to: integer_t.self, capacity: Int(count)) {
|
||||||
|
task_info(mach_task_self_, task_flavor_t(TASK_VM_INFO), $0, &count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
guard result == KERN_SUCCESS else { return 0 }
|
||||||
|
return info.phys_footprint
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -322,6 +322,9 @@ final class RDEPUBTextContentView: UIView, UIGestureRecognizerDelegate, RDReader
|
|||||||
// layouter alive for the visible page. Avoid reader-level page
|
// layouter alive for the visible page. Avoid reader-level page
|
||||||
// caching so preloading does not retain several full-chapter
|
// caching so preloading does not retain several full-chapter
|
||||||
// display copies at once.
|
// display copies at once.
|
||||||
|
// Do NOT relax this until chapter-level shared display content
|
||||||
|
// lands (LONG_CHAPTER_MEMORY_OPTIMIZATION_PLAN.md P1-1): cached
|
||||||
|
// pages would each pin their own full-chapter copy again.
|
||||||
return currentPage == nil
|
return currentPage == nil
|
||||||
|| loadingSpinner.isAnimating
|
|| loadingSpinner.isAnimating
|
||||||
|| coreTextDisplayContent != nil
|
|| coreTextDisplayContent != nil
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user