diff --git a/.planning/phases/08-pagination-quality-cache-performance/08-01-PLAN.md b/.planning/phases/08-pagination-quality-cache-performance/08-01-PLAN.md
new file mode 100644
index 0000000..529a3da
--- /dev/null
+++ b/.planning/phases/08-pagination-quality-cache-performance/08-01-PLAN.md
@@ -0,0 +1,122 @@
+---
+phase: 08-pagination-quality-cache-performance
+plan: 01
+type: execute
+wave: 1
+depends_on: []
+files_modified:
+ - Sources/RDReaderView/EPUBTextRendering/RDEPUBTextRendererCache.swift
+autonomous: true
+requirements:
+ - QUAL-01
+must_haves:
+ truths:
+ - "Cache key struct exists that captures viewport + typography configuration"
+ - "Cache lookup and store methods exist on the cache type"
+ - "Build succeeds with new cache file in project"
+ artifacts:
+ - path: "Sources/RDReaderView/EPUBTextRendering/RDEPUBTextRendererCache.swift"
+ provides: "Cache key struct and cache manager stub"
+ exports: ["RDEPUBTextRendererCache", "RDEPUBPaginationCacheKey"]
+ key_links:
+ - from: "Sources/RDReaderView/EPUBTextRendering/RDEPUBTextRendererCache.swift"
+ to: "RDEPUBDTCoreTextRenderer"
+ via: "import (future wiring)"
+ pattern: "RDEPUBTextRendererCache"
+---
+
+
+Create the pagination cache infrastructure: a cache key struct that captures viewport dimensions and typography configuration, plus a cache manager stub with lookup/store/invalidate methods. This is the structural foundation that plan 08-02 and future work will wire into the actual rendering pipeline.
+
+Purpose: Prevents the same chapter from being fully re-typeset when viewport and typography config haven't changed (QUAL-01).
+
+Output: `RDEPUBTextRendererCache.swift` with cache key struct and manager stub.
+
+
+
+@$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/RDEPUBDTCoreTextRenderer.swift:
+- `RDEPUBTextChapterRenderRequest` — contains `style: RDEPUBTextRenderStyle`, `context` with baseURL
+- `RDEPUBTextRenderStyle` — has `font: UIFont`, `lineSpacing: CGFloat`, `textColor: UIColor?`, `backgroundColor: UIColor?`
+
+From Sources/RDReaderView/EPUBTextRendering/RDEPUBTextRendererSupport.swift:
+- `RDEPUBTextRenderStyle` properties used in rendering: `font.pointSize`, `font.familyName`, `font.fontName`, `lineSpacing`
+
+
+
+
+
+
+ Task 1: Create cache key struct and cache manager stub
+ Sources/RDReaderView/EPUBTextRendering/RDEPUBTextRendererCache.swift
+
+ - Sources/RDReaderView/EPUBTextRendering/RDEPUBDTCoreTextRenderer.swift (understand render request and style types)
+ - Sources/RDReaderView/EPUBTextRendering/RDEPUBTextRendererSupport.swift (understand what configuration affects pagination output)
+
+
+Create `Sources/RDReaderView/EPUBTextRendering/RDEPUBTextRendererCache.swift` with:
+
+1. `RDEPUBPaginationCacheKey` struct — captures the inputs that determine pagination output:
+ - `chapterID: String` (unique chapter identifier, e.g. href or file path)
+ - `viewportSize: CGSize` (the available page dimensions)
+ - `fontDescriptor: String` (font family + name + pointSize, concatenated)
+ - `lineSpacing: CGFloat`
+ - `textColorHash: Int?` (hash of textColor, if present)
+ - Implement `Equatable` conformance (auto-synthesize is fine)
+
+2. `RDEPUBTextRendererCache` class:
+ - `static let shared = RDEPUBTextRendererCache()`
+ - `private var cache: [RDEPUBPaginationCacheKey: [NSAttributedString]]` — maps key to array of page attributed strings
+ - `func cachedPages(for key: RDEPUBPaginationCacheKey) -> [NSAttributedString]?` — returns cached pages or nil
+ - `func store(pages: [NSAttributedString], for key: RDEPUBPaginationCacheKey)` — stores pages
+ - `func invalidate(for key: RDEPUBPaginationCacheKey)` — removes a specific entry
+ - `func invalidateAll()` — clears entire cache
+ - `var entryCount: Int` — read-only count of cached entries (for diagnostics)
+
+Make the class `final` and `Sendable`-safe (use a lock or `@MainActor` annotation since rendering is main-thread). Use `NSLock` for thread safety.
+
+Do NOT wire this cache into the rendering pipeline yet — that is future work. This plan only creates the infrastructure.
+
+
+ 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
+ - Class has methods: `cachedPages(for:)`, `store(pages:for:)`, `invalidate(for:)`, `invalidateAll()`
+ - Class has computed property `entryCount: Int`
+ - Build succeeds (xcodebuild exits 0)
+
+ Cache key struct and cache manager stub exist, build compiles successfully
+
+
+
+
+
+- Build succeeds with new file
+- Cache types are properly defined and accessible from other modules in the same target
+
+
+
+- RDEPUBPaginationCacheKey captures all inputs that affect pagination output
+- RDEPUBTextRendererCache provides a thread-safe, singleton cache surface ready for wiring
+- No existing code is modified — this is purely additive
+
+
+
diff --git a/.planning/phases/08-pagination-quality-cache-performance/08-02-PLAN.md b/.planning/phases/08-pagination-quality-cache-performance/08-02-PLAN.md
new file mode 100644
index 0000000..3f4aae7
--- /dev/null
+++ b/.planning/phases/08-pagination-quality-cache-performance/08-02-PLAN.md
@@ -0,0 +1,297 @@
+---
+phase: 08-pagination-quality-cache-performance
+plan: 02
+type: execute
+wave: 1
+depends_on: []
+files_modified:
+ - Sources/RDReaderView/EPUBTextRendering/RDEPUBTextRendererSupport.swift
+ - Sources/RDReaderView/EPUBTextRendering/RDEPUBDTCoreTextRenderer.swift
+ - Sources/RDReaderView/EPUBUI/RDEPUBTextContentView.swift
+autonomous: false
+requirements:
+ - QUAL-02
+ - QUAL-03
+ - QUAL-04
+must_haves:
+ truths:
+ - "All image attachments are constrained to max 1080x1920 via aspect-ratio-preserving scaling"
+ - "Footnote images use width-only sizing (width:1em in CSS), no explicit height"
+ - "DTMaxImageSize in dtOptions is 1080x1920, not screen bounds"
+ - "Cover images still display correctly using screen-size base, capped by unified max"
+ - "qrbodyPic images no longer overflow page height"
+ artifacts:
+ - path: "Sources/RDReaderView/EPUBTextRendering/RDEPUBTextRendererSupport.swift"
+ provides: "Unified max-size constraint in prepareHTMLElementForReaderRendering, cleaned-up footnote handling"
+ contains: "CGSize(width: 1080, height: 1920)"
+ - path: "Sources/RDReaderView/EPUBTextRendering/RDEPUBDTCoreTextRenderer.swift"
+ provides: "Updated DTMaxImageSize from screen bounds to 1080x1920"
+ contains: "CGSize(width: 1080, height: 1920)"
+ key_links:
+ - from: "RDEPUBDTCoreTextRenderer.dtOptions"
+ to: "DTMaxImageSize"
+ via: "builder option"
+ pattern: "DTMaxImageSize.*CGSize.*1080"
+ - from: "RDEPUBTextRendererSupport.prepareHTMLElementForReaderRendering"
+ to: "DTTextAttachment.displaySize"
+ via: "unified max-size scaling"
+ pattern: "maxImageSize.*1080"
+---
+
+
+Fix image display for all three image types (qrbodyPic, cover, footnote) by applying a unified maximum size constraint (1080x1920) aligned with WXRead's `_WRPostProcessElementTree`, and simplifying footnote sizing to width-only per WXRead's `replace.css`.
+
+Purpose: Complex illustrated chapters currently have images that overflow pages, inconsistent footnote sizing, and no unified ceiling. This fixes QUAL-02 (pagination quality), QUAL-03 (verifiable image rules), and addresses QUAL-04 (lightweight operation, no perf impact).
+
+Output: Updated `RDEPUBTextRendererSupport.swift` (main changes), `RDEPUBDTCoreTextRenderer.swift` (DTMaxImageSize), `RDEPUBTextContentView.swift` (remove footnote override).
+
+
+
+@$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 DTCoreText (CocoaPod, DTTextAttachment):
+- `attachment.originalSize: CGSize` — the image's intrinsic pixel size
+- `attachment.displaySize: CGSize` — the size used for layout (set by code)
+- `attachment.verticalAlignment: DTTextAttachmentVerticalAlignment` — .center, .baseline, etc.
+- `element.displayStyle: DTHTMLElementDisplayStyle` — .inline, .block
+- `element.fontDescriptor: UIFontDescriptor` — font info from the element
+- `element.textAttachment: DTTextAttachment?` — the attachment on the element
+
+From RDEPUBTextRendererSupport.swift (current implementation, lines 217-262):
+```swift
+static func prepareHTMLElementForReaderRendering(
+ _ element: DTHTMLElement,
+ style: RDEPUBTextRenderStyle
+) {
+ guard let attachment = element.textAttachment else { return }
+ // ... class/path detection, pointSize calculation ...
+ // footnote: height calc pointSize * 0.54, displaySize set
+ // cover: UIScreen.main.bounds.insetBy(dx: 20, dy: 28).size scaling
+}
+```
+
+From RDEPUBDTCoreTextRenderer.swift (current implementation, line 100):
+```swift
+DTMaxImageSize: NSValue(cgSize: screenBounds.size) // ~353x757
+```
+
+From RDEPUBTextContentView.swift (current implementation, lines 263-282):
+```swift
+private func normalizeInlineAttachments(in content: NSMutableAttributedString, basePointSize: CGFloat) {
+ // footnote: targetHeight = pointSize * 0.14, displaySize set
+}
+```
+
+From RDEPUBTextRendererSupport.swift (current implementation, lines 467-505):
+```swift
+private static func normalizeAttachmentDisplayIfNeeded(
+ in attributes: inout [NSAttributedString.Key: Any],
+ font: UIFont
+) {
+ // footnote: targetHeight = font.pointSize * 0.14
+ // cover: maxWidth = max(UIScreen.main.bounds.width - 48, font.lineHeight * 8)
+}
+```
+
+WXRead reference (Doc/WXRead/decompiled/WREpubTypesetter.m §672-701):
+- Checks only `originalSize.width > maxSize.width` (not height)
+- Max size is `CGSizeMake(1080, 1920)`
+- Scale: `maxSize.width / originalSize.width`
+- Sets `displaySize` to scaled size
+
+
+
+
+
+
+ Task 1: Apply unified max-size constraint and fix footnote sizing across 3 files
+
+ Sources/RDReaderView/EPUBTextRendering/RDEPUBTextRendererSupport.swift,
+ Sources/RDReaderView/EPUBTextRendering/RDEPUBDTCoreTextRenderer.swift,
+ Sources/RDReaderView/EPUBUI/RDEPUBTextContentView.swift
+
+
+ - Sources/RDReaderView/EPUBTextRendering/RDEPUBTextRendererSupport.swift (full file — contains prepareHTMLElementForReaderRendering at L217, replaceCSS at L302, normalizeAttachmentHTMLMarkers at L345, normalizeAttachmentDisplayIfNeeded at L467)
+ - Sources/RDReaderView/EPUBTextRendering/RDEPUBDTCoreTextRenderer.swift (L90-112 — dtOptions with DTMaxImageSize)
+ - Sources/RDReaderView/EPUBUI/RDEPUBTextContentView.swift (L263-282 — normalizeInlineAttachments)
+ - Doc/WXRead/decompiled/WREpubTypesetter.m §672-701 (reference: _WRPostProcessElementTree max size logic)
+ - Doc/WXRead/resources/css/replace.css §91-103 (reference: .qqreader-footnote CSS)
+
+
+ - Test: DTMaxImageSize in dtOptions() is CGSize(width: 1080, height: 1920), not screen bounds
+ - Test: prepareHTMLElementForReaderRendering applies unified max-size scaling before any type-specific handling
+ - Test: Footnote branch in prepareHTMLElementForReaderRendering has NO height calculation (no pointSize * 0.54)
+ - Test: Footnote branch only sets verticalAlignment = .center and displayStyle = .inline, then returns
+ - Test: normalizeAttachmentHTMLMarkers footnote regex adds styleFragments with "width:1em" but NOT "height:1em"
+ - Test: normalizeInlineAttachments does NOT contain pointSize * 0.14 height override for footnotes
+ - Test: normalizeAttachmentDisplayIfNeeded does NOT contain font.pointSize * 0.14 height override for footnotes
+
+
+Apply changes across three files. Each change is described with its exact location and target state.
+
+**File 1: RDEPUBDTCoreTextRenderer.swift — line 100**
+
+Change `DTMaxImageSize` value from `NSValue(cgSize: screenBounds.size)` to `NSValue(cgSize: CGSize(width: 1080, height: 1920))`. The `screenBounds` variable at line 92 can remain (used elsewhere or for future reference), but `DTMaxImageSize` must use the new constant. Per D-01.
+
+**File 2: RDEPUBTextRendererSupport.swift — 4 locations**
+
+**Location A: prepareHTMLElementForReaderRendering (L217-262) — Add unified max-size block**
+
+Insert a new block immediately after `guard let attachment = element.textAttachment else { return }` (line 221), BEFORE the existing class/path detection (line 223). The block:
+
+```
+let maxImageSize = CGSize(width: 1080, height: 1920)
+let originalSize = attachment.originalSize
+if originalSize.width > 0, originalSize.height > 0,
+ (originalSize.width > maxImageSize.width || originalSize.height > maxImageSize.height) {
+ let scale = min(maxImageSize.width / originalSize.width,
+ maxImageSize.height / originalSize.height)
+ attachment.displaySize = CGSize(
+ width: round(originalSize.width * scale),
+ height: round(originalSize.height * scale)
+ )
+}
+```
+
+Per D-01, D-02, D-03. This applies to ALL images before any type-specific handling.
+
+**Location B: prepareHTMLElementForReaderRendering footnote branch (L228-241) — Remove height calculation**
+
+Current code at lines 229-233:
+```swift
+let targetHeight = max(round(pointSize * 0.54), 1)
+let originalSize = attachment.originalSize
+let aspectRatio = originalSize.height > 0 ? originalSize.width / originalSize.height : 1
+let targetWidth = max(round(targetHeight * max(aspectRatio, 0.1)), 1)
+attachment.displaySize = CGSize(width: targetWidth, height: targetHeight)
+```
+
+Replace with: remove the height/width calculation entirely. Keep only `attachment.verticalAlignment = .center` and `element.displayStyle = .inline`. The `return` at the end stays. Per D-07, D-08. Width is already set via HTML preprocessing (`width:1em` in normalizeAttachmentHTMLMarkers). DTCoreText auto-calculates height from aspect ratio when only width is set.
+
+Also remove the `pointSize` local variable usage from this branch — `pointSize` is still used by the logging line, so keep the variable declaration but it's no longer used for sizing.
+
+**Location C: normalizeAttachmentHTMLMarkers (L359-361) — Remove height:1em from footnote style fragments**
+
+Current styleFragments at line 360-361:
+```swift
+"width:1em",
+"height:1em",
+```
+
+Change to:
+```swift
+"width:1em",
+```
+
+Remove `"height:1em"` line. Per D-10.
+
+**Location D: normalizeAttachmentDisplayIfNeeded (L477-482) — Remove footnote height override**
+
+Lines 477-483 currently compute footnote displaySize with `font.pointSize * 0.14`. Remove the entire `if lowercasedClasses.contains("qqreader-footnote") || lowercasedPath == "note.png"` block (lines 477-483). The footnote is already handled correctly by the unified max-size + width-only CSS approach in prepareHTMLElementForReaderRendering and normalizeAttachmentHTMLMarkers. Per D-08, D-09.
+
+Keep the cover path (lines 484-494) — it acts as a fallback and does not conflict.
+
+**File 3: RDEPUBTextContentView.swift — normalizeInlineAttachments (L263-282)**
+
+Remove the footnote height override logic inside `normalizeInlineAttachments`. Lines 270-278:
+```swift
+guard classes.contains("qqreader-footnote") || source.contains("note.png") else { return }
+let pointSize = max(basePointSize, 1)
+let targetHeight = max(round(pointSize * 0.14), 1)
+...
+attachment.displaySize = CGSize(width: targetWidth, height: targetHeight)
+```
+
+Replace the entire DTTextAttachment branch body with just a guard + return (skip footnote attachments entirely — they're already sized correctly from the renderer pipeline). The function should still iterate but do nothing for footnotes. Per D-09.
+
+**Build verification:** After all changes, run `xcodebuild build` to confirm compilation.
+
+
+ xcodebuild build -project ReadViewDemo/ReadViewDemo.xcodeproj -scheme ReadViewDemo -destination 'platform=iOS Simulator,name=iPhone 16' 2>&1 | tail -5
+
+
+ - RDEPUBDTCoreTextRenderer.swift: DTMaxImageSize line contains `CGSize(width: 1080, height: 1920)` (not screenBounds.size)
+ - RDEPUBTextRendererSupport.swift: prepareHTMLElementForReaderRendering contains `let maxImageSize = CGSize(width: 1080, height: 1920)` BEFORE any class/path detection
+ - RDEPUBTextRendererSupport.swift: prepareHTMLElementForReaderRendering footnote branch does NOT contain `pointSize * 0.54` or any `targetHeight`/`targetWidth` calculation
+ - RDEPUBTextRendererSupport.swift: prepareHTMLElementForReaderRendering footnote branch contains `attachment.verticalAlignment = .center` and `element.displayStyle = .inline`
+ - RDEPUBTextRendererSupport.swift: normalizeAttachmentHTMLMarkers footnote styleFragments contain `"width:1em"` but NOT `"height:1em"`
+ - RDEPUBTextRendererSupport.swift: normalizeAttachmentDisplayIfNeeded does NOT contain `pointSize * 0.14` or `font.pointSize * 0.14` for footnote path
+ - RDEPUBTextContentView.swift: normalizeInlineAttachments does NOT contain `pointSize * 0.14` height override for footnotes
+ - xcodebuild build exits 0
+
+ All three image sizing code paths are updated: unified 1080x1920 max-size in prepareHTMLElementForReaderRendering, DTMaxImageSize set to 1080x1920, footnote height overrides removed from all three locations, footnote CSS uses width-only (width:1em), build compiles
+
+
+
+ Task 2: Visual verification of image display fix
+
+Unified image sizing constraint applied across the native text rendering pipeline:
+- All images constrained to max 1080x1920 (WXRead alignment)
+- Footnote images: width-only sizing (width:1em), height auto-calculated from aspect ratio
+- Cover images: screen-size base, capped by unified max
+- qrbodyPic images: constrained by unified max (no more height overflow)
+
+Changed files:
+- RDEPUBDTCoreTextRenderer.swift (DTMaxImageSize)
+- RDEPUBTextRendererSupport.swift (prepareHTMLElementForReaderRendering, normalizeAttachmentHTMLMarkers, normalizeAttachmentDisplayIfNeeded)
+- RDEPUBTextContentView.swift (normalizeInlineAttachments)
+
+
+1. Build and run ReadViewDemo on simulator
+2. Open the demo book "宝山辽墓材料与释读"
+3. Navigate to the cover page — verify cover image displays correctly (not cropped, not stretched, centered)
+4. Navigate to Chapter 5 (has qrbodyPic images) — verify images display fully within page bounds, no height overflow
+5. Navigate to a chapter with footnote images — verify footnote images are inline with text, same height as text characters, not oversized
+6. Switch to dark mode — verify images still display correctly
+7. Check that page breaks around images are reasonable (no orphan images at page tops/bottoms)
+
+ Type "approved" if all image types display correctly, or describe which image type has issues
+
+
+
+
+
+## Trust Boundaries
+
+| Boundary | Description |
+|----------|-------------|
+| EPUB HTML input -> DTCoreText parser | Untrusted HTML/CSS from EPUB files enters the rendering pipeline |
+| DTCoreText attachment -> willFlushCallback | Image dimensions from EPUB metadata could be arbitrary |
+
+## STRIDE Threat Register
+
+| Threat ID | Category | Component | Disposition | Mitigation Plan |
+|-----------|----------|-----------|-------------|-----------------|
+| T-08-01 | Tampering | Image size from EPUB metadata | mitigate | Unified max-size constraint (1080x1920) caps any image regardless of declared dimensions |
+| T-08-02 | Denial of Service | Maliciously large images | mitigate | DTMaxImageSize (first pass) + willFlushCallback max-size (second pass) ensure no image exceeds 1080x1920 display size |
+| T-08-SC | Tampering | npm/pip/cargo installs | accept | No new packages installed in this phase |
+
+
+
+- xcodebuild build succeeds
+- Visual inspection: cover, qrbodyPic, and footnote images all display correctly
+- No regressions in dark mode
+
+
+
+- All images constrained to 1080x1920 max (QUAL-03: verifiable processing rules)
+- Complex illustrated chapters display without image overflow or unreasonable whitespace (QUAL-02)
+- Image sizing is a lightweight operation in willFlushCallback with negligible perf impact (QUAL-04)
+- Footnote images are inline, text-height, width-only (WXRead alignment)
+
+
+
diff --git a/.planning/phases/08-pagination-quality-cache-performance/08-03-PLAN.md b/.planning/phases/08-pagination-quality-cache-performance/08-03-PLAN.md
new file mode 100644
index 0000000..70bba4d
--- /dev/null
+++ b/.planning/phases/08-pagination-quality-cache-performance/08-03-PLAN.md
@@ -0,0 +1,121 @@
+---
+phase: 08-pagination-quality-cache-performance
+plan: 03
+type: execute
+wave: 1
+depends_on: []
+files_modified:
+ - Sources/RDReaderView/EPUBTextRendering/RDEPUBTextRendererSupport.swift
+autonomous: true
+requirements:
+ - QUAL-04
+must_haves:
+ truths:
+ - "Pagination timing is logged with chapter ID, page count, and elapsed milliseconds"
+ - "Diagnostic summary includes per-chapter pagination stats"
+ artifacts:
+ - path: "Sources/RDReaderView/EPUBTextRendering/RDEPUBTextRendererSupport.swift"
+ provides: "Pagination timing instrumentation in existing rendering path"
+ contains: "[EPUB][Perf]"
+ key_links:
+ - from: "RDEPUBTextRendererSupport"
+ to: "os.signpost / print"
+ via: "timing instrumentation"
+ pattern: "\\[EPUB\\]\\[Perf\\]"
+---
+
+
+Add performance sampling and diagnostic output to the pagination pipeline: log chapter ID, page count, and elapsed time for each pagination pass. This provides the baseline data needed to detect regressions and validate that quality improvements don't degrade performance.
+
+Purpose: QUAL-04 requires that pagination improvements don't significantly degrade first-screen time, re-pagination time, or interaction smoothness. Without measurement, this can't be verified.
+
+Output: Timing instrumentation in the existing rendering path with `[EPUB][Perf]` log prefix.
+
+
+
+@$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 RDEPUBTextRendererSupport.swift:
+- The main pagination entry point processes a chapter HTML string and produces an array of pages
+- Existing logging pattern: `print("[EPUB][Attachment] footnote classes=...")`
+- Use the same `print("[EPUB]...")` convention for consistency
+
+From RDEPUBDTCoreTextRenderer.swift:
+- `RDEPUBTextChapterRenderRequest` contains chapter identification
+- Rendering produces `NSAttributedString` per page
+
+
+
+
+
+
+ Task 1: Add pagination timing instrumentation
+ Sources/RDReaderView/EPUBTextRendering/RDEPUBTextRendererSupport.swift
+
+ - Sources/RDReaderView/EPUBTextRendering/RDEPUBTextRendererSupport.swift (full file — find the main pagination entry point)
+ - Sources/RDReaderView/EPUBTextRendering/RDEPUBDTCoreTextRenderer.swift (understand render request flow and chapter identification)
+
+
+Add timing instrumentation to the pagination pipeline in `RDEPUBTextRendererSupport.swift`.
+
+**What to instrument:** The function that takes chapter HTML and produces paginated output (the main entry point called by the renderer). Wrap the core pagination logic with `CFAbsoluteTimeGetCurrent()` before and after.
+
+**What to log:** After pagination completes, print a single diagnostic line:
+```
+[EPUB][Perf] chapter={chapterIdentifier} pages={pageCount} elapsed={milliseconds}ms
+```
+
+Where:
+- `chapterIdentifier` = the chapter href or file name passed into the rendering request
+- `pageCount` = number of pages produced
+- `milliseconds` = elapsed time as a rounded integer
+
+**Implementation approach:**
+1. At the top of the pagination function, capture `let startTime = CFAbsoluteTimeGetCurrent()`
+2. At the end (after pages are produced), compute `let elapsed = Int(round((CFAbsoluteTimeGetCurrent() - startTime) * 1000))`
+3. Print: `print("[EPUB][Perf] chapter=\(chapterID) pages=\(pages.count) elapsed=\(elapsed)ms")`
+
+Use the same logging convention as the existing `[EPUB][Attachment]` logs. If the function already has a chapter identifier parameter, use it directly. If not, extract it from the request context.
+
+Do NOT add `os.signpost` or `OSLog` — keep it simple with `print` to match existing project conventions. A more sophisticated logging system can be added in Phase 9.
+
+
+ xcodebuild build -project ReadViewDemo/ReadViewDemo.xcodeproj -scheme ReadViewDemo -destination 'platform=iOS Simulator,name=iPhone 16' 2>&1 | tail -5
+
+
+ - RDEPUBTextRendererSupport.swift contains `CFAbsoluteTimeGetCurrent()` for timing capture
+ - RDEPUBTextRendererSupport.swift contains `print("[EPUB][Perf]` diagnostic output
+ - Diagnostic line includes chapter identifier, page count, and elapsed milliseconds
+ - xcodebuild build exits 0
+
+ Pagination timing instrumentation is in place, build compiles, diagnostic log line fires after each chapter pagination
+
+
+
+
+
+- Build succeeds
+- Running the demo app produces `[EPUB][Perf]` log lines in the console after navigating between chapters
+
+
+
+- Each pagination pass logs chapter ID, page count, and elapsed time
+- Log format is consistent with existing `[EPUB]` convention
+- No performance overhead beyond a single `CFAbsoluteTimeGetCurrent()` call (negligible)
+
+
+