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