feat: configurable chapter window & parallel metadata parsing with benchmark

1. Configurable chapter window size (onDemandChapterWindowSize: 3-15)
   - Parameterized window radius in RDEPUBChapterRuntimeStore
   - Updated RDEPUBChapterWindowCoordinator to use configurable radius
   - RDEPUBChapterWindowSnapshot.from() accepts chapter array instead of fixed prev/next
   - Even numbers round up to odd (4→5), min 3, max 15

2. Configurable metadata parsing concurrency (metadataParsingConcurrency)
   - Default equals CPU core count
   - Parallel execution via OperationQueue in paginateMetadataOnly
   - Each worker creates independent builder instance
   - NSLock protects result aggregation

3. Per-chapter and total wall-clock timing instrumentation
   - Separated render vs I/O timing per chapter
   - Summary log with wallClockMs, renderTotalMs, writeTotalMs, avgRenderMs
   - Timing stored in RDEPUBReaderContext for test access

4. UI automation test infrastructure
   - Added --demo-window-size, --demo-concurrency, --demo-clear-cache launch args
   - DemoReaderState exposes windowSize, parseMs, parseConcurrency
   - ConfigurableWindowTests: 5 test cases for window size 3/5/15
   - ConcurrentParsingTests: 4 test cases for concurrency 2/4
   - MetadataParseBenchmarkTests: serial vs parallel benchmark

5. Bug fixes
   - Fixed page snap-back during background parsing (isUserInteracting check)
   - Reduced BookPageMap refresh frequency from 16 to 32 chapters
   - Moved waitForReadingInteractionToSettle outside operation loop

6. Design doc: dual-layer PageMap (estimated + precise mixed)
This commit is contained in:
shen
2026-06-03 23:38:11 +08:00
parent feb05eaf87
commit d20196ee34
17 changed files with 932 additions and 153 deletions
@@ -107,6 +107,12 @@ public struct RDEPUBReaderConfiguration: Equatable {
public var fixedLayoutSpreadMode: RDEPUBFixedLayoutSpreadMode
/// 使 DTCoreText
public var textRenderingEngine: RDEPUBTextRenderingEngine
/// 3 3...15
public var onDemandChapterWindowSize: Int
/// CPU
/// profiling renderTotalMs wallClockMs
/// writeTotalMs I/O cpuCount * 1.25~1.5 I/O
public var metadataParsingConcurrency: Int
// MARK:
@@ -128,6 +134,8 @@ public struct RDEPUBReaderConfiguration: Equatable {
/// - fixedLayoutFit:
/// - fixedLayoutSpreadMode:
/// - textRenderingEngine:
/// - onDemandChapterWindowSize: 3...15
/// - metadataParsingConcurrency: CPU
public init(
fontSize: CGFloat = 15,
lineHeightMultiple: CGFloat = 1.6,
@@ -146,7 +154,9 @@ public struct RDEPUBReaderConfiguration: Equatable {
darkImageBlendRatio: CGFloat = 0.15,
fixedLayoutFit: RDEPUBFixedLayoutFit = .page,
fixedLayoutSpreadMode: RDEPUBFixedLayoutSpreadMode = .automatic,
textRenderingEngine: RDEPUBTextRenderingEngine = .dtCoreText
textRenderingEngine: RDEPUBTextRenderingEngine = .dtCoreText,
onDemandChapterWindowSize: Int = 3,
metadataParsingConcurrency: Int = ProcessInfo.processInfo.activeProcessorCount
) {
self.fontSize = fontSize
self.lineHeightMultiple = lineHeightMultiple
@@ -166,12 +176,25 @@ public struct RDEPUBReaderConfiguration: Equatable {
self.fixedLayoutFit = fixedLayoutFit
self.fixedLayoutSpreadMode = fixedLayoutSpreadMode
self.textRenderingEngine = textRenderingEngine
self.onDemandChapterWindowSize = Self.normalizedChapterWindowSize(onDemandChapterWindowSize)
self.metadataParsingConcurrency = max(1, metadataParsingConcurrency)
}
/// 使
public static let `default` = RDEPUBReaderConfiguration()
}
extension RDEPUBReaderConfiguration {
static func normalizedChapterWindowSize(_ size: Int) -> Int {
let clamped = max(3, min(15, size))
return clamped % 2 == 0 ? clamped + 1 : clamped
}
var chapterWindowRadius: Int {
onDemandChapterWindowSize / 2
}
}
// MARK: -
extension RDEPUBReaderConfiguration {