ReadViewSDK/ReadViewDemo/ReadViewDemoUITests/Helpers/DemoReaderState.swift
shen d20196ee34 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)
2026-06-03 23:38:11 +08:00

75 lines
2.9 KiB
Swift

import XCTest
struct DemoReaderState {
let rawValue: String
let fields: [String: String]
init(rawValue: String) {
self.rawValue = rawValue
var parsed: [String: String] = [:]
let separators = CharacterSet.whitespacesAndNewlines.union(CharacterSet(charactersIn: ";"))
for token in rawValue.components(separatedBy: separators) where !token.isEmpty {
guard let delimiterIndex = token.firstIndex(of: "=") else { continue }
let key = String(token[..<delimiterIndex])
let value = String(token[token.index(after: delimiterIndex)...])
parsed[key] = value
}
fields = parsed
}
subscript(key: String) -> String? {
fields[key]
}
var isOpened: Bool { fields["reader"] == "opened" }
var page: Int? { fields["page"].flatMap(Int.init) }
var display: String? { fields["display"] }
var toolbar: String? { fields["toolbar"] }
var highlights: Int? { fields["highlights"].flatMap(Int.init) }
var selection: Int? { fields["selection"].flatMap(Int.init) }
var href: String? { fields["href"] }
var progression: Double? { fields["progression"].flatMap(Double.init) }
var mode: String? { fields["mode"] }
var pagination: String? { fields["pagination"] }
var knownPages: Int? { fields["knownPages"].flatMap(Int.init) }
var knownChapters: Int? { fields["knownChapters"].flatMap(Int.init) }
var buildableChapters: Int? { fields["buildableChapters"].flatMap(Int.init) }
var avoidWidows: Int? { fields["avoidWidows"].flatMap(Int.init) }
var avoidOrphans: Int? { fields["avoidOrphans"].flatMap(Int.init) }
var windowSize: Int? { fields["windowSize"].flatMap(Int.init) }
var parseMs: Int? { fields["parseMs"].flatMap(Int.init) }
var parseConcurrency: Int? { fields["parseConcurrency"].flatMap(Int.init) }
}
extension XCUIApplication {
func currentDemoReaderState() -> DemoReaderState? {
let state = staticTexts[IDs.demoReaderState]
guard state.exists else { return nil }
return DemoReaderState(rawValue: state.label)
}
@discardableResult
func waitForDemoReaderState(
timeout: TimeInterval = 8,
description: String,
where predicate: (DemoReaderState) -> Bool
) -> DemoReaderState {
let stateElement = staticTexts[IDs.demoReaderState]
let deadline = Date().addingTimeInterval(timeout)
var lastState = DemoReaderState(rawValue: "<missing>")
while Date() < deadline {
if stateElement.exists {
lastState = DemoReaderState(rawValue: stateElement.label)
if predicate(lastState) {
return lastState
}
}
RunLoop.current.run(until: Date().addingTimeInterval(0.1))
}
XCTFail("阅读器状态未满足条件: \(description),当前:\(lastState.rawValue)")
return lastState
}
}