feat(wxread): align pagination, rendering, and docs
This commit is contained in:
@@ -2,33 +2,50 @@
|
||||
phase: 08-pagination-quality-cache-performance
|
||||
plan: 03
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: []
|
||||
wave: 2
|
||||
depends_on:
|
||||
- 08-01
|
||||
- 08-02
|
||||
files_modified:
|
||||
- Sources/RDReaderView/EPUBTextRendering/RDEPUBTextRendererSupport.swift
|
||||
- Sources/RDReaderView/EPUBTextRendering/RDEPUBTextPerformanceSampler.swift
|
||||
- Sources/RDReaderView/EPUBTextRendering/RDEPUBTextBookBuilder.swift
|
||||
autonomous: true
|
||||
requirements:
|
||||
- QUAL-01
|
||||
- QUAL-04
|
||||
must_haves:
|
||||
truths:
|
||||
- "Pagination timing is logged with chapter ID, page count, and elapsed milliseconds"
|
||||
- "Builder records per-chapter render time and paginate time"
|
||||
- "Builder records total build time"
|
||||
- "Builder records cache hit/miss per chapter"
|
||||
- "Performance samples are accessible via lastBuildPerformanceSamples"
|
||||
- "Cache integration: builder accepts optional cache parameter and skips build on hit"
|
||||
- "Cache miss triggers full build and stores result"
|
||||
- "Console logs show [PERF] summary with timing data"
|
||||
artifacts:
|
||||
- path: "Sources/RDReaderView/EPUBTextRendering/RDEPUBTextRendererSupport.swift"
|
||||
provides: "Pagination timing instrumentation in existing rendering path"
|
||||
contains: "[EPUB][Perf]"
|
||||
- path: "Sources/RDReaderView/EPUBTextRendering/RDEPUBTextPerformanceSampler.swift"
|
||||
provides: "RDEPUBTextPerformanceSample struct and RDEPUBTextPerformanceSampler class"
|
||||
contains: "struct RDEPUBTextPerformanceSample"
|
||||
- path: "Sources/RDReaderView/EPUBTextRendering/RDEPUBTextBookBuilder.swift"
|
||||
provides: "Cache integration and performance instrumentation in build()"
|
||||
contains: "lastBuildPerformanceSamples"
|
||||
key_links:
|
||||
- from: "RDEPUBTextRendererSupport"
|
||||
to: "os.signpost / print"
|
||||
via: "timing instrumentation"
|
||||
pattern: "\\[EPUB\\]\\[Perf\\]"
|
||||
- from: "RDEPUBTextBookBuilder.swift"
|
||||
to: "RDEPUBTextBookCache.swift"
|
||||
via: "cache.load(key:) / cache.save(_:key:)"
|
||||
pattern: "cache\\.load\\|cache\\.save"
|
||||
- from: "RDEPUBTextBookBuilder.swift"
|
||||
to: "RDEPUBTextPerformanceSampler.swift"
|
||||
via: "sampler.record(sample)"
|
||||
pattern: "sampler\\.record"
|
||||
---
|
||||
|
||||
<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.
|
||||
Create performance sampling infrastructure and integrate cache + performance instrumentation into RDEPUBTextBookBuilder.build(). This adds timing measurements for render/paginate/build operations and wires the cache from Plan 01 so that repeated builds with the same parameters skip the full pipeline.
|
||||
|
||||
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.
|
||||
Purpose: QUAL-04 requires stable diagnostic/sampling data to ensure no degradation. QUAL-01 requires cache integration to avoid redundant pagination. This plan brings both into the builder.
|
||||
|
||||
Output: Timing instrumentation in the existing rendering path with `[EPUB][Perf]` log prefix.
|
||||
Output: New RDEPUBTextPerformanceSampler.swift; modified RDEPUBTextBookBuilder.swift with cache integration and timing instrumentation.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@@ -42,77 +59,336 @@ Output: Timing instrumentation in the existing rendering path with `[EPUB][Perf]
|
||||
@.planning/STATE.md
|
||||
@.planning/phases/08-pagination-quality-cache-performance/08-CONTEXT.md
|
||||
@.planning/phases/08-pagination-quality-cache-performance/08-RESEARCH.md
|
||||
@.planning/phases/08-pagination-quality-cache-performance/08-PATTERNS.md
|
||||
@.planning/phases/08-pagination-quality-cache-performance/08-01-SUMMARY.md
|
||||
@.planning/phases/08-pagination-quality-cache-performance/08-02-SUMMARY.md
|
||||
@Sources/RDReaderView/EPUBTextRendering/RDEPUBTextBookBuilder.swift
|
||||
@Sources/RDReaderView/EPUBTextRendering/RDEPUBTextBookCache.swift
|
||||
@Sources/RDReaderView/EPUBTextRendering/RDEPUBTextPerformanceSampler.swift
|
||||
@Sources/RDReaderView/EPUBTextRendering/RDEPUBTextRenderer.swift
|
||||
|
||||
<interfaces>
|
||||
<!-- Key types the executor needs -->
|
||||
<!-- Builder init pattern. From RDEPUBTextBookBuilder.swift lines 101-107 -->
|
||||
```swift
|
||||
public init(renderer: RDEPUBTextRenderer) {
|
||||
self.renderer = renderer
|
||||
}
|
||||
|
||||
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
|
||||
public convenience init() {
|
||||
self.init(renderer: RDEPUBDTCoreTextRenderer())
|
||||
}
|
||||
```
|
||||
|
||||
From RDEPUBDTCoreTextRenderer.swift:
|
||||
- `RDEPUBTextChapterRenderRequest` contains chapter identification
|
||||
- Rendering produces `NSAttributedString` per page
|
||||
<!-- Existing diagnostics properties. From RDEPUBTextBookBuilder.swift lines 98-99 -->
|
||||
```swift
|
||||
public private(set) var lastBuildResourceDiagnostics: [RDEPUBTextResourceReferenceDiagnostic] = []
|
||||
public private(set) var lastBuildPaginationDiagnostics: [RDEPUBTextChapterPaginationDiagnostic] = []
|
||||
```
|
||||
|
||||
<!-- Build method signature. From RDEPUBTextBookBuilder.swift lines 132-137 -->
|
||||
```swift
|
||||
public func build(
|
||||
parser: RDEPUBParser,
|
||||
publication: RDEPUBPublication,
|
||||
pageSize: CGSize,
|
||||
style: RDEPUBTextRenderStyle
|
||||
) throws -> RDEPUBTextBook
|
||||
```
|
||||
|
||||
<!-- Cache public API (from Plan 01) -->
|
||||
```swift
|
||||
public final class RDEPUBTextBookCache {
|
||||
public init(subdirectory: String = "RDEPUBTextBookCache")
|
||||
public func cacheKey(bookID: String, fontSize: CGFloat, lineHeightMultiple: CGFloat, contentInsets: UIEdgeInsets, pageSize: CGSize) -> String
|
||||
public func load(key: String) -> RDEPUBTextBook?
|
||||
public func save(_ book: RDEPUBTextBook, key: String)
|
||||
public func invalidateAll()
|
||||
public var schemaVersion: Int
|
||||
}
|
||||
```
|
||||
|
||||
<!-- Performance measurement pattern (standard iOS) -->
|
||||
```swift
|
||||
let start = CFAbsoluteTimeGetCurrent()
|
||||
// ... operation ...
|
||||
let duration = CFAbsoluteTimeGetCurrent() - start
|
||||
```
|
||||
|
||||
<!-- Existing struct pattern for diagnostics. From RDEPUBTextRenderer.swift lines 90-113 -->
|
||||
```swift
|
||||
public struct RDEPUBTextResourceReferenceDiagnostic: Equatable {
|
||||
public var kind: RDEPUBTextResourceReferenceKind
|
||||
public var chapterHref: String
|
||||
// ...
|
||||
public init(kind: ..., chapterHref: ..., ...) { ... }
|
||||
}
|
||||
```
|
||||
|
||||
<!-- PaginationSupport extension (from Plan 02, Task 1 — updated to pass config) -->
|
||||
```swift
|
||||
extension NSAttributedString {
|
||||
func rd_paginatedFrames(
|
||||
size: CGSize,
|
||||
fragmentOffsets: [String: Int] = [:],
|
||||
config: RDEPUBTextLayoutConfig = .default
|
||||
) -> [RDEPUBTextLayoutFrame]
|
||||
}
|
||||
```
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Add pagination timing instrumentation</name>
|
||||
<files>Sources/RDReaderView/EPUBTextRendering/RDEPUBTextRendererSupport.swift</files>
|
||||
<name>Task 1: Create RDEPUBTextPerformanceSampler with timing sample struct and recording API</name>
|
||||
<files>Sources/RDReaderView/EPUBTextRendering/RDEPUBTextPerformanceSampler.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)
|
||||
Sources/RDReaderView/EPUBTextRendering/RDEPUBTextRenderer.swift
|
||||
Sources/RDReaderView/EPUBTextRendering/RDEPUBTextBookBuilder.swift
|
||||
</read_first>
|
||||
<action>
|
||||
Add timing instrumentation to the pagination pipeline in `RDEPUBTextRendererSupport.swift`.
|
||||
Create a new file `Sources/RDReaderView/EPUBTextRendering/RDEPUBTextPerformanceSampler.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.
|
||||
**RDEPUBTextPerformanceSample struct** (per QUAL-04, D-04):
|
||||
|
||||
**What to log:** After pagination completes, print a single diagnostic line:
|
||||
```
|
||||
[EPUB][Perf] chapter={chapterIdentifier} pages={pageCount} elapsed={milliseconds}ms
|
||||
```swift
|
||||
public struct RDEPUBTextPerformanceSample: Equatable {
|
||||
public var chapterHref: String
|
||||
public var renderDuration: TimeInterval // DTCoreText render time
|
||||
public var paginateDuration: TimeInterval // CoreText pagination time
|
||||
public var pageCount: Int
|
||||
public var attributedStringLength: Int
|
||||
public var cacheHit: Bool
|
||||
|
||||
public init(
|
||||
chapterHref: String,
|
||||
renderDuration: TimeInterval,
|
||||
paginateDuration: TimeInterval,
|
||||
pageCount: Int,
|
||||
attributedStringLength: Int,
|
||||
cacheHit: Bool
|
||||
) { ... }
|
||||
}
|
||||
```
|
||||
|
||||
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
|
||||
Follow the exact pattern from `RDEPUBTextResourceReferenceDiagnostic` at RDEPUBTextRenderer.swift lines 90-113: public struct, Equatable, explicit public init with all properties.
|
||||
|
||||
**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")`
|
||||
**RDEPUBTextPerformanceSampler class**:
|
||||
|
||||
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.
|
||||
```swift
|
||||
public final class RDEPUBTextPerformanceSampler {
|
||||
public private(set) var samples: [RDEPUBTextPerformanceSample] = []
|
||||
public private(set) var totalBuildDuration: TimeInterval = 0
|
||||
|
||||
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.
|
||||
public init() {}
|
||||
|
||||
public func record(_ sample: RDEPUBTextPerformanceSample) {
|
||||
samples.append(sample)
|
||||
print("[PERF] \(sample.chapterHref): render=\(formatMS(sample.renderDuration)) paginate=\(formatMS(sample.paginateDuration)) pages=\(sample.pageCount) cache=\(sample.cacheHit ? "HIT" : "MISS")")
|
||||
}
|
||||
|
||||
public func summary() -> String {
|
||||
let totalRender = samples.reduce(0) { $0 + $1.renderDuration }
|
||||
let totalPaginate = samples.reduce(0) { $0 + $1.paginateDuration }
|
||||
let hitCount = samples.filter(\.cacheHit).count
|
||||
return "[PERF] chapters=\(samples.count) render=\(formatMS(totalRender)) paginate=\(formatMS(totalPaginate)) total=\(formatMS(totalBuildDuration)) cacheHits=\(hitCount)/\(samples.count)"
|
||||
}
|
||||
|
||||
public func reset() {
|
||||
samples.removeAll()
|
||||
totalBuildDuration = 0
|
||||
}
|
||||
|
||||
private func formatMS(_ duration: TimeInterval) -> String {
|
||||
String(format: "%.0fms", duration * 1000)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Console output pattern** (follows existing `[EPUB]` style in the codebase):
|
||||
- Each `record()` call prints a per-chapter `[PERF]` line
|
||||
- `summary()` returns a formatted string (caller prints it)
|
||||
- Use milliseconds (multiply TimeInterval by 1000) for human-readable output
|
||||
</action>
|
||||
<verify>
|
||||
<automated>xcodebuild build -project ReadViewDemo/ReadViewDemo.xcodeproj -scheme ReadViewDemo -destination 'platform=iOS Simulator,name=iPhone 16' 2>&1 | tail -5</automated>
|
||||
<automated>xcodebuild build -scheme ReadViewDemo -destination 'platform=iOS Simulator,name=iPhone 17' 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
|
||||
- File `Sources/RDReaderView/EPUBTextRendering/RDEPUBTextPerformanceSampler.swift` exists
|
||||
- `RDEPUBTextPerformanceSample` is a public struct with Equatable conformance
|
||||
- Properties: `chapterHref`, `renderDuration`, `paginateDuration`, `pageCount`, `attributedStringLength`, `cacheHit`
|
||||
- `RDEPUBTextPerformanceSampler` is a public final class
|
||||
- `record(_:)` method appends sample and prints `[PERF]` line
|
||||
- `summary()` returns formatted string with totals and cache hit rate
|
||||
- `reset()` clears samples and totalBuildDuration
|
||||
- `totalBuildDuration` is a public property
|
||||
- Build succeeds
|
||||
</acceptance_criteria>
|
||||
<done>Pagination timing instrumentation is in place, build compiles, diagnostic log line fires after each chapter pagination</done>
|
||||
<done>
|
||||
RDEPUBTextPerformanceSampler.swift contains performance sample struct and sampler class with record/summary/reset API and [PERF] console logging. Build succeeds.
|
||||
</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Integrate cache and performance sampling into RDEPUBTextBookBuilder.build()</name>
|
||||
<files>Sources/RDReaderView/EPUBTextRendering/RDEPUBTextBookBuilder.swift</files>
|
||||
<read_first>
|
||||
Sources/RDReaderView/EPUBTextRendering/RDEPUBTextBookBuilder.swift
|
||||
Sources/RDReaderView/EPUBTextRendering/RDEPUBTextBookCache.swift
|
||||
Sources/RDReaderView/EPUBTextRendering/RDEPUBTextPerformanceSampler.swift
|
||||
Sources/RDReaderView/EPUBTextRendering/RDEPUBTextRenderer.swift
|
||||
Sources/RDReaderView/EPUBUI/RDEPUBReaderController.swift
|
||||
</read_first>
|
||||
<action>
|
||||
Modify `RDEPUBTextBookBuilder.swift` to integrate the cache (from Plan 01) and performance sampling (Task 1).
|
||||
|
||||
**Step 1: Add new stored properties** (per D-01, D-04)
|
||||
|
||||
After the existing `lastBuildPaginationDiagnostics` property (line 99), add:
|
||||
|
||||
```swift
|
||||
public private(set) var lastBuildPerformanceSamples: [RDEPUBTextPerformanceSample] = []
|
||||
public private(set) var lastBuildCacheStats: (hits: Int, misses: Int) = (0, 0)
|
||||
```
|
||||
|
||||
**Step 2: Add cache and sampler to init** (per D-01)
|
||||
|
||||
Update the initializer to accept optional cache:
|
||||
|
||||
```swift
|
||||
private let cache: RDEPUBTextBookCache?
|
||||
private let sampler: RDEPUBTextPerformanceSampler
|
||||
|
||||
public init(renderer: RDEPUBTextRenderer, cache: RDEPUBTextBookCache? = nil) {
|
||||
self.renderer = renderer
|
||||
self.cache = cache
|
||||
self.sampler = RDEPUBTextPerformanceSampler()
|
||||
}
|
||||
|
||||
public convenience init() {
|
||||
self.init(renderer: RDEPUBDTCoreTextRenderer())
|
||||
}
|
||||
```
|
||||
|
||||
The `cache` parameter defaults to nil for backward compatibility. The sampler is always created (lightweight, no cost until samples recorded).
|
||||
|
||||
**Step 3: Add cache key helper**
|
||||
|
||||
Add a private helper method to generate the cache key from build parameters:
|
||||
|
||||
```swift
|
||||
private func makeCacheKey(
|
||||
bookID: String,
|
||||
pageSize: CGSize,
|
||||
style: RDEPUBTextRenderStyle
|
||||
) -> String? {
|
||||
guard let cache else { return nil }
|
||||
return cache.cacheKey(
|
||||
bookID: bookID,
|
||||
fontSize: style.font.pointSize,
|
||||
lineHeightMultiple: style.lineSpacing,
|
||||
contentInsets: .zero, // or derive from pageSize insets if available
|
||||
pageSize: pageSize
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
Note: The `bookID` needs to come from the publication. Use `publication.metadata.identifier ?? publication.metadata.title ?? "unknown"` as the bookID. If no identifier is available, return nil to skip caching.
|
||||
|
||||
**Step 4: Instrument the build() method** (per QUAL-04, QUAL-01)
|
||||
|
||||
At the start of `build()`, after the existing `lastBuildResourceDiagnostics = []` / `lastBuildPaginationDiagnostics = []` lines:
|
||||
|
||||
1. **Reset sampler**: `sampler.reset()`, `lastBuildCacheStats = (0, 0)`
|
||||
2. **Start build timer**: `let buildStart = CFAbsoluteTimeGetCurrent()`
|
||||
3. **Cache lookup**: If cache is available, generate key and try `cache.load(key:)`. If hit, set `lastBuildCacheStats.hits += 1`, print `[Cache] HIT`, and return the cached book immediately.
|
||||
4. **Cache miss**: If cache miss, proceed with existing build loop.
|
||||
|
||||
Inside the for loop, around each chapter's render + paginate:
|
||||
|
||||
1. **Render timing**: Wrap `renderer.renderChapter(request:)` (line 158) with `CFAbsoluteTimeGetCurrent()` before and after
|
||||
2. **Paginate timing**: Wrap `content.rd_paginatedFrames(size:)` (line 196) with `CFAbsoluteTimeGetCurrent()` before and after
|
||||
3. **Record sample**: After getting layoutFrames, record:
|
||||
```swift
|
||||
sampler.record(RDEPUBTextPerformanceSample(
|
||||
chapterHref: item.href,
|
||||
renderDuration: renderDuration,
|
||||
paginateDuration: paginateDuration,
|
||||
pageCount: effectiveFrames.count,
|
||||
attributedStringLength: content.length,
|
||||
cacheHit: false
|
||||
))
|
||||
```
|
||||
4. **Miss counter**: `lastBuildCacheStats.misses += 1`
|
||||
|
||||
After the loop, before returning:
|
||||
|
||||
1. **Set total build duration**: `sampler.totalBuildDuration = CFAbsoluteTimeGetCurrent() - buildStart`
|
||||
2. **Save to cache**: If cache available and key was generated, `cache.save(book, key: cacheKey)`
|
||||
3. **Print summary**: `print(sampler.summary())`
|
||||
4. **Store samples**: `lastBuildPerformanceSamples = sampler.samples`
|
||||
|
||||
**Step 5: Update build() method signature** — keep the existing signature unchanged. The cache is accessed via the stored property, not a method parameter.
|
||||
|
||||
**Important implementation notes**:
|
||||
- All timing uses `CFAbsoluteTimeGetCurrent()` (no external deps, per D-04)
|
||||
- Timing happens on the same queue as the build (DispatchQueue.global(qos: .userInitiated)), not dispatched to main
|
||||
- The `publication.metadata.identifier` or equivalent needs to be accessible — check the `RDEPUBPublication` type for an identifier field
|
||||
- If publication has no identifier, skip cache (return nil from makeCacheKey)
|
||||
</action>
|
||||
<verify>
|
||||
<automated>xcodebuild build -scheme ReadViewDemo -destination 'platform=iOS Simulator,name=iPhone 17' 2>&1 | tail -5</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `RDEPUBTextBookBuilder` has `lastBuildPerformanceSamples: [RDEPUBTextPerformanceSample]` property
|
||||
- `RDEPUBTextBookBuilder` has `lastBuildCacheStats: (hits: Int, misses: Int)` property
|
||||
- `RDEPUBTextBookBuilder` init accepts optional `cache: RDEPUBTextBookCache?` parameter (defaults to nil)
|
||||
- `build()` method contains `CFAbsoluteTimeGetCurrent()` timing calls for render and paginate
|
||||
- `build()` method calls `cache.load(key:)` before the build loop when cache is available
|
||||
- `build()` method calls `cache.save(_:key:)` after the build loop when cache is available
|
||||
- `build()` method calls `sampler.record()` for each chapter with timing data
|
||||
- `build()` method prints `sampler.summary()` at the end
|
||||
- Existing `convenience init()` still works (cache defaults to nil)
|
||||
- Build succeeds with no regressions
|
||||
</acceptance_criteria>
|
||||
<done>
|
||||
RDEPUBTextBookBuilder.build() is instrumented with per-chapter render/paginate timing, cache hit/miss logging, and performance sample recording. Cache integration skips build on hit and stores result on miss. Build succeeds.
|
||||
</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| builder → cache | Builder writes to cache after successful build; corrupted cache could return invalid RDEPUBTextBook on next load |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Disposition | Mitigation |
|
||||
|-----------|----------|-----------|-------------|------------|
|
||||
| T-08-04 | Tampering | Cache integration in builder | accept | Cache is optional (nil default); NSKeyedArchiver deserialization fails safely returning nil |
|
||||
| T-08-SC | Tampering | npm/pip/cargo installs | mitigate | No external packages installed in this phase |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
- Build succeeds
|
||||
- Running the demo app produces `[EPUB][Perf]` log lines in the console after navigating between chapters
|
||||
- `xcodebuild build -scheme ReadViewDemo` succeeds
|
||||
- Builder init accepts optional cache parameter
|
||||
- build() method has CFAbsoluteTimeGetCurrent timing instrumentation
|
||||
- Cache load/save calls are present in build() flow
|
||||
- Performance samples accessible via lastBuildPerformanceSamples
|
||||
- Console shows [PERF] summary and [Cache] HIT/MISS logs
|
||||
</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)
|
||||
- Per-chapter render and paginate durations are recorded
|
||||
- Total build time is captured
|
||||
- Cache hit skips the entire build loop
|
||||
- Cache miss runs full build and stores result
|
||||
- Performance summary printed to console after each build
|
||||
- No functional regression: book opens, paginates, and displays correctly
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
|
||||
Reference in New Issue
Block a user