3 plans covering QUAL-01 through QUAL-04: - 08-01: pagination cache key + wiring into rendering pipeline - 08-02: unified 1080x1920 max-size for all images, footnote width-only fix - 08-03: pagination timing instrumentation Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
11 KiB
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | must_haves | ||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 08-pagination-quality-cache-performance | 01 | execute | 1 |
|
true |
|
|
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.
<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_context>
@.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.mdFrom Sources/RDReaderView/EPUBTextRendering/RDEPUBTextRenderer.swift:
RDEPUBTextChapterRenderRequest— containscontext: RDEPUBTextChapterContext,style: RDEPUBTextRenderStyleRDEPUBTextChapterContext— hashref: String,title: String,html: String,baseURL: URL?RDEPUBTextRenderStyle— hasfont: UIFont,lineSpacing: CGFloat,textColor: UIColor?,backgroundColor: UIColor?RDEPUBRenderedChapterContent— hasattributedString: 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 itemscontent.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
-
RDEPUBPaginationCacheKeystruct — captures the inputs that determine pagination output:chapterID: String(the chapter href fromRDEPUBTextChapterContext.href)viewportSize: CGSize(thepageSizepassed tobuildBook)fontDescriptor: String(font family + name + pointSize, concatenated, e.g."\(font.familyName)-\(font.fontName)-\(font.pointSize)")lineSpacing: CGFloattextColorHash: Int(hash of textColor, or 0 if nil)- Implement
Equatableconformance (auto-synthesize is fine since all stored properties are Equatable)
-
RDEPUBTextRendererCacheclass:static let shared = RDEPUBTextRendererCache()private var cache: [RDEPUBPaginationCacheKey: [RDEPUBTextLayoutFrame]]— maps key to array of layout framesprivate let lock = NSLock()— thread safetyfunc cachedFrames(for key: RDEPUBPaginationCacheKey) -> [RDEPUBTextLayoutFrame]?— lock, lookup, unlock, returnfunc store(frames: [RDEPUBTextLayoutFrame], for key: RDEPUBPaginationCacheKey)— lock, store, unlockfunc invalidate(for key: RDEPUBPaginationCacheKey)— lock, remove, unlockfunc invalidateAll()— lock, removeAll, unlockvar 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
<acceptance_criteria>
- 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)
</acceptance_criteria>
Cache key struct and cache manager exist with correct value types, build compiles successfully
Location: Inside the buildBook function, around the existing pagination block (lines 194-198):
layoutFrames = content.length > 0
? content.rd_paginatedFrames(size: pageSize, fragmentOffsets: rendered.fragmentOffsets)
: []
Replace with cache-aware logic:
-
Before the pagination call, build a cache key:
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 ) -
Check cache first:
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) } } -
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
<acceptance_criteria>
- 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)
</acceptance_criteria>
Cache is wired into the pagination pipeline — repeated pagination of the same chapter with identical config returns cached frames, build compiles
<success_criteria>
- 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) </success_criteria>