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)
98 lines
4.2 KiB
Swift
98 lines
4.2 KiB
Swift
import XCTest
|
||
|
||
final class MetadataParseBenchmarkTests: XCTestCase {
|
||
private let app = XCUIApplication()
|
||
private let largeBookQuery = "凡人修仙传"
|
||
private let parseTimeout: TimeInterval = 900
|
||
|
||
override func setUpWithError() throws {
|
||
continueAfterFailure = false
|
||
}
|
||
|
||
/// 单次解析基准:用指定并发数打开大书,等待解析完成,返回 parseMs
|
||
private func runParseBenchmark(concurrency: Int) throws -> (parseMs: Int, concurrency: Int) {
|
||
app.launchAndOpenSampleBook(
|
||
bookTitleQuery: largeBookQuery,
|
||
resetsReaderState: true,
|
||
concurrency: concurrency,
|
||
clearsCache: true
|
||
)
|
||
app.waitForReader(timeout: 20)
|
||
|
||
// 等待首屏可读
|
||
_ = app.waitForDemoReaderState(timeout: 15, description: "首屏加载 concurrency=\(concurrency)") { state in
|
||
state.mode == "bookPageMap" && (state.page ?? 0) >= 1
|
||
}
|
||
|
||
// 等待 parseMs 写入(OperationQueue 完成时设置,早于 pagination=full)
|
||
let completed = app.waitForDemoReaderState(timeout: parseTimeout, description: "解析完成 concurrency=\(concurrency)") { state in
|
||
(state.parseMs ?? 0) > 0
|
||
}
|
||
|
||
let parseMs = completed.parseMs ?? 0
|
||
let actualConcurrency = completed.parseConcurrency ?? 0
|
||
|
||
XCTAssertTrue(parseMs > 0, "parseMs 应大于 0")
|
||
XCTAssertEqual(actualConcurrency, concurrency, "实际并发数应等于配置值")
|
||
|
||
// pagination=full 可能因个别章节失败而不达到,不阻塞基准测试
|
||
|
||
// 返回书架
|
||
app.showReaderChromeIfNeeded()
|
||
if app.buttons[IDs.readerBack].waitForExistence(timeout: 5) {
|
||
app.buttons[IDs.readerBack].tap()
|
||
}
|
||
XCTAssertTrue(app.tables[IDs.demoBooksTable].waitForExistence(timeout: 10), "返回书架失败")
|
||
|
||
return (parseMs, actualConcurrency)
|
||
}
|
||
|
||
func testMetadataParseBenchmark() throws {
|
||
let cpuCount = ProcessInfo.processInfo.activeProcessorCount
|
||
|
||
// 1. 串行基准
|
||
let serial = try runParseBenchmark(concurrency: 1)
|
||
print("[Benchmark] concurrency=1 parseMs=\(serial.parseMs)")
|
||
|
||
// 2. CPU 核心数并发
|
||
let parallel = try runParseBenchmark(concurrency: cpuCount)
|
||
print("[Benchmark] concurrency=\(cpuCount) parseMs=\(parallel.parseMs)")
|
||
|
||
// 3. 加速比
|
||
let speedup = Double(serial.parseMs) / max(Double(parallel.parseMs), 1)
|
||
let efficiency = speedup / Double(cpuCount) * 100
|
||
|
||
print("[Benchmark] ---- 结果 ----")
|
||
print("[Benchmark] CPU cores: \(cpuCount)")
|
||
print("[Benchmark] serial(1): \(serial.parseMs)ms")
|
||
print("[Benchmark] parallel(\(cpuCount)): \(parallel.parseMs)ms")
|
||
print("[Benchmark] speedup: \(String(format: "%.2f", speedup))x")
|
||
print("[Benchmark] efficiency: \(String(format: "%.1f", efficiency))%")
|
||
|
||
if efficiency > 70 {
|
||
print("[Benchmark] 结论: 渲染受限,并发数=\(cpuCount) 合理")
|
||
} else if efficiency > 40 {
|
||
print("[Benchmark] 结论: 有 I/O 等待,可试探 concurrency=\(Int(Double(cpuCount) * 1.25))~\(Int(Double(cpuCount) * 1.5))")
|
||
} else {
|
||
print("[Benchmark] 结论: I/O 或锁竞争严重,建议降低并发数或排查瓶颈")
|
||
}
|
||
|
||
// 并行应该比串行快
|
||
XCTAssertTrue(parallel.parseMs < serial.parseMs, "并发解析(\(parallel.parseMs)ms)应快于串行(\(serial.parseMs)ms)")
|
||
}
|
||
|
||
func testMetadataParseScaling() throws {
|
||
let cpuCount = ProcessInfo.processInfo.activeProcessorCount
|
||
let concurrency1 = try runParseBenchmark(concurrency: 1)
|
||
let concurrencyN = try runParseBenchmark(concurrency: cpuCount)
|
||
|
||
let speedup = Double(concurrency1.parseMs) / max(Double(concurrencyN.parseMs), 1)
|
||
|
||
print("[Scaling] concurrency=1: \(concurrency1.parseMs)ms")
|
||
print("[Scaling] concurrency=\(cpuCount): \(concurrencyN.parseMs)ms")
|
||
print("[Scaling] speedup: \(String(format: "%.2f", speedup))x on \(cpuCount) cores")
|
||
|
||
XCTAssertTrue(speedup >= 1.5, "并发加速比(\(String(format: "%.2f", speedup)))过低,可能存在瓶颈")
|
||
}
|
||
}
|