Files
ReadViewSDK/ReadViewDemo/ReadViewDemoUITests/ReaderUITests/MetadataParseBenchmarkTests.swift
T
shenandshen 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)))过低,可能存在瓶颈")
}
}