ReadViewSDK/ReadViewDemo/ReadViewDemoUITests/ReaderUITests/MetadataParseBenchmarkTests.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

98 lines
4.2 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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)))过低,可能存在瓶颈")
}
}