--- phase: 08-pagination-quality-cache-performance plan: 01 type: execute wave: 1 depends_on: [] files_modified: - Sources/RDReaderView/EPUBTextRendering/RDEPUBTextRendererCache.swift - Sources/RDReaderView/EPUBTextRendering/RDEPUBTextBookBuilder.swift autonomous: true requirements: - QUAL-01 must_haves: truths: - "Repeated pagination of the same chapter with unchanged viewport and typography returns cached layout frames without re-typesetting" - "Changing font size or viewport dimensions invalidates the cache and triggers fresh pagination" - "Cache lookup is transparent — downstream consumers receive the same RDEPUBTextLayoutFrame array whether cached or freshly computed" artifacts: - path: "Sources/RDReaderView/EPUBTextRendering/RDEPUBTextRendererCache.swift" provides: "Cache key struct and cache manager for pagination layout frames" exports: ["RDEPUBTextRendererCache", "RDEPUBPaginationCacheKey"] - path: "Sources/RDReaderView/EPUBTextRendering/RDEPUBTextBookBuilder.swift" provides: "Cache wiring in pagination pipeline" contains: "RDEPUBTextRendererCache.shared" key_links: - from: "Sources/RDReaderView/EPUBTextRendering/RDEPUBTextBookBuilder.swift" to: "Sources/RDReaderView/EPUBTextRendering/RDEPUBTextRendererCache.swift" via: "cache lookup before rd_paginatedFrames, store after" pattern: "RDEPUBTextRendererCache\\.shared" - from: "Sources/RDReaderView/EPUBTextRendering/RDEPUBTextRendererCache.swift" to: "RDEPUBTextLayoutFrame" via: "cached value type" pattern: "\\[RDEPUBTextLayoutFrame\\]" --- Create the pagination cache infrastructure and wire it into the rendering pipeline so that repeated pagination of the same chapter with unchanged viewport and typography configuration returns cached layout frames without re-typesetting. Purpose: Prevents the same chapter from being fully re-typeset when viewport and typography config haven't changed (QUAL-01). The cache wraps the `rd_paginatedFrames` call in `RDEPUBTextBookBuilder`, which is the actual pagination bottleneck. Output: `RDEPUBTextRendererCache.swift` with cache key struct and manager, plus updated `RDEPUBTextBookBuilder.swift` with cache wiring. @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/08-pagination-quality-cache-performance/08-CONTEXT.md @.planning/phases/08-pagination-quality-cache-performance/08-RESEARCH.md From Sources/RDReaderView/EPUBTextRendering/RDEPUBTextRenderer.swift: - `RDEPUBTextChapterRenderRequest` — contains `context: RDEPUBTextChapterContext`, `style: RDEPUBTextRenderStyle` - `RDEPUBTextChapterContext` — has `href: String`, `title: String`, `html: String`, `baseURL: URL?` - `RDEPUBTextRenderStyle` — has `font: UIFont`, `lineSpacing: CGFloat`, `textColor: UIColor?`, `backgroundColor: UIColor?` - `RDEPUBRenderedChapterContent` — has `attributedString: NSAttributedString`, `fragmentOffsets: [String: Int]` From Sources/RDReaderView/EPUBTextRendering/RDEPUBTextLayoutFrame.swift: - `struct RDEPUBTextLayoutFrame: Equatable` — value type with contentRange, breakReason, attachmentRanges, diagnostics, etc. From Sources/RDReaderView/EPUBTextRendering/RDEPUBTextBookBuilder.swift (line 135, 196): - `buildBook(... pageSize: CGSize, ...)` — the function that iterates spine items - `content.rd_paginatedFrames(size: pageSize, fragmentOffsets: rendered.fragmentOffsets)` — the pagination call to cache around From Sources/RDReaderView/EPUBTextRendering/RDEPUBTextPaginationSupport.swift: - `rd_paginatedFrames(size: CGSize, fragmentOffsets: [String: Int]) -> [RDEPUBTextLayoutFrame]` — extension on NSAttributedString Task 1: Create cache key struct and cache manager Sources/RDReaderView/EPUBTextRendering/RDEPUBTextRendererCache.swift - Sources/RDReaderView/EPUBTextRendering/RDEPUBTextRenderer.swift (understand RDEPUBTextRenderStyle, RDEPUBTextChapterRenderRequest, RDEPUBTextChapterContext) - Sources/RDReaderView/EPUBTextRendering/RDEPUBTextLayoutFrame.swift (understand the value type to cache) - Sources/RDReaderView/EPUBTextRendering/RDEPUBTextBookBuilder.swift (lines 130-200 — understand pageSize and rd_paginatedFrames usage) Create `Sources/RDReaderView/EPUBTextRendering/RDEPUBTextRendererCache.swift` with: 1. `RDEPUBPaginationCacheKey` struct — captures the inputs that determine pagination output: - `chapterID: String` (the chapter href from `RDEPUBTextChapterContext.href`) - `viewportSize: CGSize` (the `pageSize` passed to `buildBook`) - `fontDescriptor: String` (font family + name + pointSize, concatenated, e.g. `"\(font.familyName)-\(font.fontName)-\(font.pointSize)"`) - `lineSpacing: CGFloat` - `textColorHash: Int` (hash of textColor, or 0 if nil) - Implement `Equatable` conformance (auto-synthesize is fine since all stored properties are Equatable) 2. `RDEPUBTextRendererCache` class: - `static let shared = RDEPUBTextRendererCache()` - `private var cache: [RDEPUBPaginationCacheKey: [RDEPUBTextLayoutFrame]]` — maps key to array of layout frames - `private let lock = NSLock()` — thread safety - `func cachedFrames(for key: RDEPUBPaginationCacheKey) -> [RDEPUBTextLayoutFrame]?` — lock, lookup, unlock, return - `func store(frames: [RDEPUBTextLayoutFrame], for key: RDEPUBPaginationCacheKey)` — lock, store, unlock - `func invalidate(for key: RDEPUBPaginationCacheKey)` — lock, remove, unlock - `func invalidateAll()` — lock, removeAll, unlock - `var entryCount: Int` — lock, read count, unlock (for diagnostics) Make the class `final`. Use `NSLock` for thread safety since the cache may be accessed from different queues. xcodebuild build -project ReadViewDemo/ReadViewDemo.xcodeproj -scheme ReadViewDemo -destination 'platform=iOS Simulator,name=iPhone 16' 2>&1 | tail -5 - `Sources/RDReaderView/EPUBTextRendering/RDEPUBTextRendererCache.swift` exists - File contains `struct RDEPUBPaginationCacheKey: Equatable` with properties: chapterID, viewportSize, fontDescriptor, lineSpacing, textColorHash - File contains `final class RDEPUBTextRendererCache` with static `shared` instance - Cache value type is `[RDEPUBTextLayoutFrame]` (not `[NSAttributedString]`) - Class has methods: `cachedFrames(for:)`, `store(frames:for:)`, `invalidate(for:)`, `invalidateAll()` - Class has computed property `entryCount: Int` - Build succeeds (xcodebuild exits 0) Cache key struct and cache manager exist with correct value types, build compiles successfully Task 2: Wire cache into pagination pipeline in RDEPUBTextBookBuilder Sources/RDReaderView/EPUBTextRendering/RDEPUBTextBookBuilder.swift - Sources/RDReaderView/EPUBTextRendering/RDEPUBTextBookBuilder.swift (full file — understand buildBook function, pageSize parameter, rd_paginatedFrames call at line 196) - Sources/RDReaderView/EPUBTextRendering/RDEPUBTextRendererCache.swift (created in Task 1 — understand API surface) Wire the cache into `RDEPUBTextBookBuilder.swift` around the `rd_paginatedFrames` call at line 196. **Location:** Inside the `buildBook` function, around the existing pagination block (lines 194-198): ```swift layoutFrames = content.length > 0 ? content.rd_paginatedFrames(size: pageSize, fragmentOffsets: rendered.fragmentOffsets) : [] ``` **Replace with cache-aware logic:** 1. Before the pagination call, build a cache key: ```swift let cacheKey = RDEPUBPaginationCacheKey( chapterID: item.href, viewportSize: pageSize, fontDescriptor: "\(style.font.familyName)-\(style.font.fontName)-\(style.font.pointSize)", lineSpacing: style.lineSpacing, textColorHash: style.textColor?.hashValue ?? 0 ) ``` 2. Check cache first: ```swift if let cached = RDEPUBTextRendererCache.shared.cachedFrames(for: cacheKey) { layoutFrames = cached } else { layoutFrames = content.length > 0 ? content.rd_paginatedFrames(size: pageSize, fragmentOffsets: rendered.fragmentOffsets) : [] if !layoutFrames.isEmpty { RDEPUBTextRendererCache.shared.store(frames: layoutFrames, for: cacheKey) } } ``` 3. Keep the cover chapter special-case path (lines 175-193) unchanged — it doesn't go through `rd_paginatedFrames`. The `style` variable is available in scope (it's the `style: RDEPUBTextRenderStyle` parameter of `buildBook`). The `item.href` is the chapter identifier from the spine iteration. xcodebuild build -project ReadViewDemo/ReadViewDemo.xcodeproj -scheme ReadViewDemo -destination 'platform=iOS Simulator,name=iPhone 16' 2>&1 | tail -5 - RDEPUBTextBookBuilder.swift imports or references `RDEPUBTextRendererCache` - Cache key is constructed from `item.href`, `pageSize`, `style.font`, `style.lineSpacing`, `style.textColor` - `RDEPUBTextRendererCache.shared.cachedFrames(for:)` is called before `rd_paginatedFrames` - `RDEPUBTextRendererCache.shared.store(frames:for:)` is called after successful pagination - Cover chapter special-case path is NOT modified - Build succeeds (xcodebuild exits 0) Cache is wired into the pagination pipeline — repeated pagination of the same chapter with identical config returns cached frames, build compiles - Build succeeds with both new and modified files - Cache types are properly defined and accessible from RDEPUBTextBookBuilder - Cache key captures all inputs that affect pagination output (chapterID, viewportSize, font, lineSpacing, textColor) - Cache value is [RDEPUBTextLayoutFrame] matching the rd_paginatedFrames return type - RDEPUBPaginationCacheKey captures all inputs that affect pagination output - RDEPUBTextRendererCache provides a thread-safe, singleton cache surface - Cache is wired into the actual pagination path in RDEPUBTextBookBuilder - Same chapter + same config = cache hit (no re-typesetting) - Different viewport or typography = cache miss (fresh pagination) Create `.planning/phases/08-pagination-quality-cache-performance/08-01-SUMMARY.md` when done