ReadViewSDK/.planning/phases/08-pagination-quality-cache-performance/08-01-PLAN.md
shen 22bb86959c docs(08): create phase plans for image display fix and pagination cache
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>
2026-05-23 18:43:41 +08:00

11 KiB

phase plan type wave depends_on files_modified autonomous requirements must_haves
08-pagination-quality-cache-performance 01 execute 1
Sources/RDReaderView/EPUBTextRendering/RDEPUBTextRendererCache.swift
Sources/RDReaderView/EPUBTextRendering/RDEPUBTextBookBuilder.swift
true
QUAL-01
truths artifacts key_links
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
path provides exports
Sources/RDReaderView/EPUBTextRendering/RDEPUBTextRendererCache.swift Cache key struct and cache manager for pagination layout frames
RDEPUBTextRendererCache
RDEPUBPaginationCacheKey
path provides contains
Sources/RDReaderView/EPUBTextRendering/RDEPUBTextBookBuilder.swift Cache wiring in pagination pipeline RDEPUBTextRendererCache.shared
from to via pattern
Sources/RDReaderView/EPUBTextRendering/RDEPUBTextBookBuilder.swift Sources/RDReaderView/EPUBTextRendering/RDEPUBTextRendererCache.swift cache lookup before rd_paginatedFrames, store after RDEPUBTextRendererCache.shared
from to via pattern
Sources/RDReaderView/EPUBTextRendering/RDEPUBTextRendererCache.swift RDEPUBTextLayoutFrame cached value type [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.

<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.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 <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

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):

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:

    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:

    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 <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

- 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

<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>
Create `.planning/phases/08-pagination-quality-cache-performance/08-01-SUMMARY.md` when done