优化 EPUB 翻页与后台分页刷新

This commit is contained in:
shen
2026-07-06 11:08:28 +09:00
committed by shenlei
parent 71f4feb12f
commit 9e91207011
7 changed files with 274 additions and 32 deletions
@@ -56,6 +56,8 @@ final class RDEPUBMetadataParseWorker {
private let catalog: [(key: RDEPUBChapterCacheKey, spineIndex: Int, href: String, title: String)]
private let progressLogStride = 4
init(
context: RDEPUBReaderContext,
cancellationController: RDEPUBMetadataParseCancellationController,
@@ -116,19 +118,38 @@ final class RDEPUBMetadataParseWorker {
func start(token: UUID, restoreLocation: RDEPUBLocation?) {
let cancellationController = self.cancellationController
DispatchQueue.global(qos: .utility).async { [weak self] in
guard let self else { return }
DispatchQueue.global(qos: .utility).async { [self] in
RDEPUBBackgroundTrace.log(
"Metadata",
"workerDispatched token=\(token.uuidString)"
)
let context = self.context
guard let context,
context.controller != nil,
!cancellationController.isCancelled,
context.paginationToken == token else { return }
context.paginationToken == token else {
RDEPUBBackgroundTrace.log(
"Metadata",
"workerAbortedBeforeStart reason=contextUnavailableOrTokenMismatch"
)
return
}
defer { context.runtime?.paginationCoordinator.finishMetadataParseCancellationController(cancellationController) }
guard context.controller != nil,
!cancellationController.isCancelled,
context.paginationToken == token else { return }
context.paginationToken == token else {
RDEPUBBackgroundTrace.log(
"Metadata",
"workerAbortedAfterStart reason=contextUnavailableOrTokenMismatch"
)
return
}
if let restoredPageMap = self.restoreBookPageMapIfPossible() {
RDEPUBBackgroundTrace.log(
"Metadata",
"restoreBookPageMapIfPossible hit totalChapters=\(restoredPageMap.totalChapters) totalPages=\(restoredPageMap.totalPages)"
)
DispatchQueue.main.async {
guard context.paginationToken == token,
context.controller != nil,
@@ -175,13 +196,26 @@ final class RDEPUBMetadataParseWorker {
let uncachedSpineIndices = prioritizedSpineIndices
RDEPUBBackgroundTrace.log(
"Metadata",
"waitingForInteractionCooldown elapsedSinceNavigation=\(String(format: "%.2f", context.secondsSinceLastUserNavigation())) uncached=\(uncachedSpineIndices.count)"
)
self.waitForReadingInteractionToSettle(cancellationController: cancellationController)
guard !cancellationController.isCancelled,
context.controller != nil,
context.paginationToken == token else {
RDEPUBBackgroundTrace.log(
"Metadata",
"workerAbortedDuringCooldown"
)
return
}
RDEPUBBackgroundTrace.log(
"Metadata",
"start totalBuildable=\(self.allBuildableIndices.count) cached=\(cachedSpineIndices.count) uncached=\(uncachedSpineIndices.count) concurrency=\(self.workerCount) refreshInterval=\(self.pageMapRefreshInterval)"
)
let wallClockStart = CFAbsoluteTimeGetCurrent()
var totalRenderMs: Double = 0
var totalWriteMs: Double = 0
@@ -283,6 +317,10 @@ final class RDEPUBMetadataParseWorker {
resultLock.lock()
parseState.summariesBySpineIndex[spineIndex] = renderResult
parseState.totalResolvedCount += 1
let resolvedCount = parseState.totalResolvedCount
let shouldLogProgress = resolvedCount == self.allBuildableIndices.count
|| resolvedCount == cachedSpineIndices.count + 1
|| resolvedCount % self.progressLogStride == 0
if parseState.totalResolvedCount - parseState.lastAppliedCount >= refreshInterval
|| parseState.totalResolvedCount == self.allBuildableIndices.count {
parseState.lastAppliedCount = parseState.totalResolvedCount
@@ -290,6 +328,17 @@ final class RDEPUBMetadataParseWorker {
}
resultLock.unlock()
if shouldLogProgress {
let progressPercent = Self.progressPercent(
resolved: resolvedCount,
total: self.allBuildableIndices.count
)
RDEPUBBackgroundTrace.log(
"Metadata",
"chapterReady spine=\(spineIndex) resolved=\(resolvedCount)/\(self.allBuildableIndices.count) progress=\(progressPercent)% pageCount=\(renderResult.pageCount)"
)
}
if let snapshot {
let mergeStart = CFAbsoluteTimeGetCurrent()
let partialMap = self.buildPageMap(summaries: snapshot)
@@ -297,6 +346,10 @@ final class RDEPUBMetadataParseWorker {
timingLock.lock()
totalMergeMs += mergeElapsed
timingLock.unlock()
RDEPUBBackgroundTrace.log(
"Metadata",
"partialMap resolvedChapters=\(snapshot.count) totalPages=\(partialMap.totalPages) mergeMs=\(Int(mergeElapsed))"
)
DispatchQueue.main.async {
guard context.paginationToken == token,
context.controller != nil,
@@ -314,6 +367,10 @@ final class RDEPUBMetadataParseWorker {
timingLock.lock()
failedChapters += 1
timingLock.unlock()
RDEPUBBackgroundTrace.log(
"Metadata",
"chapterFailed spine=\(spineIndex) retryScheduled=true error=\(String(describing: error))"
)
self.scheduleRetry(
spineIndex: spineIndex,
@@ -354,6 +411,10 @@ final class RDEPUBMetadataParseWorker {
let finalMergeStart = CFAbsoluteTimeGetCurrent()
let pageMap = self.buildPageMap(summaries: parseState.summariesBySpineIndex)
let finalMergeMs = Int((CFAbsoluteTimeGetCurrent() - finalMergeStart) * 1000)
RDEPUBBackgroundTrace.log(
"Metadata",
"finish resolved=\(parseState.summariesBySpineIndex.count)/\(self.allBuildableIndices.count) totalPages=\(pageMap.totalPages) elapsedMs=\(wallClockMs) renderMs=\(renderTotal) writeMs=\(writeTotal) mergeMs=\(mergeTotal + finalMergeMs) failed=\(failed)"
)
if let coverageStore = context.runtime?.backgroundCoverageStore {
let resolvedSpineIndices = Set(parseState.summariesBySpineIndex.keys)
@@ -407,10 +468,18 @@ final class RDEPUBMetadataParseWorker {
cancellationController: RDEPUBMetadataParseCancellationController
) {
guard retryCount < Self.maxRetryCount else {
RDEPUBBackgroundTrace.log(
"Metadata",
"retryAborted spine=\(spineIndex) retryCount=\(retryCount)"
)
return
}
let delay = Self.retryDelays[min(retryCount, Self.retryDelays.count - 1)]
RDEPUBBackgroundTrace.log(
"Metadata",
"retryScheduled spine=\(spineIndex) retryCount=\(retryCount + 1) delayMs=\(Int(delay * 1000))"
)
DispatchQueue.global(qos: .utility).asyncAfter(deadline: .now() + delay) { [weak self] in
guard let self, let context = self.context else { return }
@@ -474,6 +543,10 @@ final class RDEPUBMetadataParseWorker {
if shouldRefresh {
let partialMap = self.buildPageMap(summaries: parseState.summariesBySpineIndex)
RDEPUBBackgroundTrace.log(
"Metadata",
"retryPartialMap resolvedChapters=\(parseState.summariesBySpineIndex.count) totalPages=\(partialMap.totalPages)"
)
DispatchQueue.main.async {
guard context.paginationToken == self.token,
context.controller != nil,
@@ -495,6 +568,11 @@ final class RDEPUBMetadataParseWorker {
}
}
private static func progressPercent(resolved: Int, total: Int) -> Int {
guard total > 0 else { return 0 }
return Int((Double(resolved) / Double(total) * 100.0).rounded())
}
private func restoreBookPageMapIfPossible() -> RDEPUBBookPageMap? {
guard let summaryDiskCache else { return nil }
guard summaryDiskCache.isCacheComplete(keys: catalog.map(\.key)) else {