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)
74 lines
2.5 KiB
Swift
74 lines
2.5 KiB
Swift
import Foundation
|
||
|
||
struct RDEPUBChapterWindowSnapshot {
|
||
/// 窗口中的章节(有序)
|
||
let chapters: [RDEPUBRuntimeChapter]
|
||
|
||
/// 展平后的连续页数组(供 RDReaderView 消费)
|
||
/// 窗口内页码不写回 RDEPUBTextPage 模型;
|
||
/// flattenedPages 的数组下标就是窗口内连续页码(从 0 开始)。
|
||
let flattenedPages: [RDEPUBTextPage]
|
||
|
||
/// 当前章在 chapters 数组中的索引
|
||
let anchorChapterIndex: Int
|
||
|
||
/// 当前章在 flattenedPages 中的起始页码(从 0 开始,窗口内编号)
|
||
let anchorPageOffset: Int
|
||
|
||
/// 当前窗口首章的 spineIndex,用于调试日志和跨窗口映射
|
||
let windowStartSpineIndex: Int
|
||
|
||
// MARK: - 构建
|
||
|
||
/// 从章节窗口构建快照
|
||
static func from(
|
||
chapters: [RDEPUBRuntimeChapter],
|
||
anchorSpineIndex: Int
|
||
) -> RDEPUBChapterWindowSnapshot {
|
||
let sortedChapters = chapters.sorted { $0.spineIndex < $1.spineIndex }
|
||
let anchorIndex = sortedChapters.firstIndex { $0.spineIndex == anchorSpineIndex } ?? 0
|
||
let pageOffset = sortedChapters.prefix(anchorIndex).reduce(0) { $0 + $1.pages.count }
|
||
|
||
// 展平页数组
|
||
var allPages: [RDEPUBTextPage] = []
|
||
for (chIdx, ch) in sortedChapters.enumerated() {
|
||
for var page in ch.pages {
|
||
page.chapterIndex = chIdx
|
||
allPages.append(page)
|
||
}
|
||
}
|
||
|
||
let windowStartSpineIndex = sortedChapters.first?.spineIndex ?? anchorSpineIndex
|
||
|
||
return RDEPUBChapterWindowSnapshot(
|
||
chapters: sortedChapters,
|
||
flattenedPages: allPages,
|
||
anchorChapterIndex: anchorIndex,
|
||
anchorPageOffset: pageOffset,
|
||
windowStartSpineIndex: windowStartSpineIndex
|
||
)
|
||
}
|
||
|
||
// MARK: - 查询
|
||
|
||
/// 窗口内页码(即 flattenedPages 下标)-> 所属章节
|
||
func chapterForPage(flattenedPageIndex: Int) -> RDEPUBRuntimeChapter? {
|
||
var offset = 0
|
||
for ch in chapters {
|
||
if flattenedPageIndex < offset + ch.pages.count {
|
||
return ch
|
||
}
|
||
offset += ch.pages.count
|
||
}
|
||
return nil
|
||
}
|
||
|
||
/// 窗口内页码(即 flattenedPages 下标)-> 所属章节的 spineIndex
|
||
func spineIndexForPage(flattenedPageIndex: Int) -> Int? {
|
||
return chapterForPage(flattenedPageIndex: flattenedPageIndex)?.spineIndex
|
||
}
|
||
|
||
/// 总页数(窗口内)
|
||
var pageCount: Int { flattenedPages.count }
|
||
}
|