docs(08): create phase plan — image display fix, cache stub, perf sampling

Three plans for Phase 8 (分页质量、缓存与性能采样):
- 08-01: Pagination cache key struct and manager stub (QUAL-01)
- 08-02: Unified 1080x1920 max-size constraint for all images, footnote width-only sizing (QUAL-02, QUAL-03, QUAL-04)
- 08-03: Pagination timing instrumentation with [EPUB][Perf] diagnostic output (QUAL-04)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
shen 2026-05-23 18:30:56 +08:00
parent 26afb9f7dc
commit 86e6922f7b
3 changed files with 540 additions and 0 deletions

View File

@ -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"
---
<objective>
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.
</objective>
<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>
<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
<interfaces>
<!-- Existing types the executor needs to know about -->
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`
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Create cache key struct and cache manager stub</name>
<files>Sources/RDReaderView/EPUBTextRendering/RDEPUBTextRendererCache.swift</files>
<read_first>
- Sources/RDReaderView/EPUBTextRendering/RDEPUBDTCoreTextRenderer.swift (understand render request and style types)
- Sources/RDReaderView/EPUBTextRendering/RDEPUBTextRendererSupport.swift (understand what configuration affects pagination output)
</read_first>
<action>
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.
</action>
<verify>
<automated>xcodebuild build -project ReadViewDemo/ReadViewDemo.xcodeproj -scheme ReadViewDemo -destination 'platform=iOS Simulator,name=iPhone 16' 2>&1 | tail -5</automated>
</verify>
<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
- Class has methods: `cachedPages(for:)`, `store(pages:for:)`, `invalidate(for:)`, `invalidateAll()`
- Class has computed property `entryCount: Int`
- Build succeeds (xcodebuild exits 0)
</acceptance_criteria>
<done>Cache key struct and cache manager stub exist, build compiles successfully</done>
</task>
</tasks>
<verification>
- Build succeeds with new file
- Cache types are properly defined and accessible from other modules in the same target
</verification>
<success_criteria>
- 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
</success_criteria>
<output>
Create `.planning/phases/08-pagination-quality-cache-performance/08-01-SUMMARY.md` when done
</output>

View File

@ -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"
---
<objective>
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).
</objective>
<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>
<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
<interfaces>
<!-- Key types and contracts the executor needs -->
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
</interfaces>
</context>
<tasks>
<task type="auto" tdd="true">
<name>Task 1: Apply unified max-size constraint and fix footnote sizing across 3 files</name>
<files>
Sources/RDReaderView/EPUBTextRendering/RDEPUBTextRendererSupport.swift,
Sources/RDReaderView/EPUBTextRendering/RDEPUBDTCoreTextRenderer.swift,
Sources/RDReaderView/EPUBUI/RDEPUBTextContentView.swift
</files>
<read_first>
- 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)
</read_first>
<behavior>
- 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
</behavior>
<action>
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.
</action>
<verify>
<automated>xcodebuild build -project ReadViewDemo/ReadViewDemo.xcodeproj -scheme ReadViewDemo -destination 'platform=iOS Simulator,name=iPhone 16' 2>&1 | tail -5</automated>
</verify>
<acceptance_criteria>
- 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
</acceptance_criteria>
<done>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</done>
</task>
<task type="checkpoint:human-verify" gate="blocking">
<name>Task 2: Visual verification of image display fix</name>
<what-built>
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)
</what-built>
<how-to-verify>
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)
</how-to-verify>
<resume-signal>Type "approved" if all image types display correctly, or describe which image type has issues</resume-signal>
</task>
</tasks>
<threat_model>
## 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 |
</threat_model>
<verification>
- xcodebuild build succeeds
- Visual inspection: cover, qrbodyPic, and footnote images all display correctly
- No regressions in dark mode
</verification>
<success_criteria>
- 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)
</success_criteria>
<output>
Create `.planning/phases/08-pagination-quality-cache-performance/08-02-SUMMARY.md` when done
</output>

View File

@ -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\\]"
---
<objective>
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.
</objective>
<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>
<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
<interfaces>
<!-- Key types the executor needs -->
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
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Add pagination timing instrumentation</name>
<files>Sources/RDReaderView/EPUBTextRendering/RDEPUBTextRendererSupport.swift</files>
<read_first>
- 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)
</read_first>
<action>
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.
</action>
<verify>
<automated>xcodebuild build -project ReadViewDemo/ReadViewDemo.xcodeproj -scheme ReadViewDemo -destination 'platform=iOS Simulator,name=iPhone 16' 2>&1 | tail -5</automated>
</verify>
<acceptance_criteria>
- 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
</acceptance_criteria>
<done>Pagination timing instrumentation is in place, build compiles, diagnostic log line fires after each chapter pagination</done>
</task>
</tasks>
<verification>
- Build succeeds
- Running the demo app produces `[EPUB][Perf]` log lines in the console after navigating between chapters
</verification>
<success_criteria>
- 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)
</success_criteria>
<output>
Create `.planning/phases/08-pagination-quality-cache-performance/08-03-SUMMARY.md` when done
</output>