ReadViewSDK/Sources/RDReaderView/EPUBUI/ReaderController/RDEPUBBackgroundCoverageStore.swift
shenlei 15b15d0e11 Remove all RDEPUBBackgroundTrace.log and bare print diagnostic calls
Keep only the evaluateFullPageMapTakeover: fullReplace diagnostic log
in RDEPUBPageMapReconciliationCoordinator.swift for future debugging.

Simplified RDEPUBBackgroundTrace to just the log method (removed measure
and debug gating). Removed all [EPUB][...], [ReadViewDemo], and
[EPUB][Pagination] print statements across the project.

Also includes earlier bug fixes:
- Fix stale currentPageNumber in extendPartial commit
- Fix pageCurl rebindVisiblePage during transition
- Fix keepCurrentWindow not removing pending update from queue
- Fix right-aligned text (巫鸿 bug) via avoidPageBreakInside,
  tail merger, and continuation paragraph normalization
- Add text-indent reset for aligned blocks in CSS compatibility layer

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-25 18:25:22 +08:00

253 lines
8.3 KiB
Swift

import Foundation
struct RDEPUBBackgroundCoverageSegment {
let lowerSpineIndex: Int
let upperSpineIndex: Int
let pageMap: RDEPUBBookPageMap
let resolvedSpineIndices: Set<Int>
let generatedAt: CFAbsoluteTime
let renderSignature: String
let estimatedMemoryBytes: Int
func contains(spineIndex: Int) -> Bool {
spineIndex >= lowerSpineIndex && spineIndex <= upperSpineIndex
}
func distance(to spineIndex: Int) -> Int {
if contains(spineIndex: spineIndex) { return 0 }
return min(abs(spineIndex - lowerSpineIndex), abs(spineIndex - upperSpineIndex))
}
}
struct RDEPUBBackgroundCoverageStorePolicy {
let maxResidentSegments: Int
let maxChaptersPerSegment: Int
let memoryBudgetBytes: Int
static let `default` = RDEPUBBackgroundCoverageStorePolicy(
maxResidentSegments: 8,
maxChaptersPerSegment: 256,
memoryBudgetBytes: 8 * 1024 * 1024
)
}
final class RDEPUBBackgroundCoverageStore {
private unowned let context: RDEPUBReaderContext
private let policy: RDEPUBBackgroundCoverageStorePolicy
private var segments: [RDEPUBBackgroundCoverageSegment] = []
private var currentMemoryBytes: Int = 0
private var lastAccessTime: [Int: CFAbsoluteTime] = [:]
init(context: RDEPUBReaderContext, policy: RDEPUBBackgroundCoverageStorePolicy = .default) {
self.context = context
self.policy = policy
}
func addSegment(_ segment: RDEPUBBackgroundCoverageSegment) {
evictIfNeeded(forNewSegment: segment)
var merged = false
for (index, existing) in segments.enumerated() {
if canMerge(existing, segment) {
if let mergedSegment = mergeSegments(existing, segment) {
segments[index] = mergedSegment
currentMemoryBytes = currentMemoryBytes - existing.estimatedMemoryBytes + mergedSegment.estimatedMemoryBytes
merged = true
break
}
}
}
if !merged {
segments.append(segment)
currentMemoryBytes += segment.estimatedMemoryBytes
}
lastAccessTime[segment.lowerSpineIndex] = CFAbsoluteTimeGetCurrent()
}
func findSegment(containing spineIndex: Int) -> RDEPUBBackgroundCoverageSegment? {
let segment = segments.first { $0.contains(spineIndex: spineIndex) }
if let segment {
lastAccessTime[segment.lowerSpineIndex] = CFAbsoluteTimeGetCurrent()
}
return segment
}
func findSegment(covering spineIndices: Set<Int>) -> RDEPUBBackgroundCoverageSegment? {
let segment = segments.first { segment in
spineIndices.allSatisfy { segment.contains(spineIndex: $0) }
}
if let segment {
lastAccessTime[segment.lowerSpineIndex] = CFAbsoluteTimeGetCurrent()
}
return segment
}
func allSegments() -> [RDEPUBBackgroundCoverageSegment] {
segments
}
func clearAll() {
segments.removeAll()
currentMemoryBytes = 0
lastAccessTime.removeAll()
}
func clearColdSegments(
activeWindowSpineIndices: Set<Int>,
protectedSpineIndices: Set<Int>
) {
let keepIndices = activeWindowSpineIndices.union(protectedSpineIndices)
segments.removeAll { segment in
let isCold = !segment.resolvedSpineIndices.contains(where: { keepIndices.contains($0) })
if isCold {
currentMemoryBytes -= segment.estimatedMemoryBytes
lastAccessTime.removeValue(forKey: segment.lowerSpineIndex)
}
return isCold
}
}
func handleMemoryWarning(
activeWindowSpineIndices: Set<Int>,
protectedSpineIndices: Set<Int>
) {
clearColdSegments(
activeWindowSpineIndices: activeWindowSpineIndices,
protectedSpineIndices: protectedSpineIndices
)
if currentMemoryBytes > policy.memoryBudgetBytes {
let sorted = segments.sorted { lhs, rhs in
let lhsDistance = lhs.resolvedSpineIndices.map { idx in
activeWindowSpineIndices.map { abs(idx - $0) }.min() ?? Int.max
}.min() ?? Int.max
let rhsDistance = rhs.resolvedSpineIndices.map { idx in
activeWindowSpineIndices.map { abs(idx - $0) }.min() ?? Int.max
}.min() ?? Int.max
return lhsDistance > rhsDistance
}
for segment in sorted {
if currentMemoryBytes <= policy.memoryBudgetBytes { break }
currentMemoryBytes -= segment.estimatedMemoryBytes
lastAccessTime.removeValue(forKey: segment.lowerSpineIndex)
segments.removeAll { $0.lowerSpineIndex == segment.lowerSpineIndex }
}
}
}
private func evictIfNeeded(forNewSegment newSegment: RDEPUBBackgroundCoverageSegment) {
while segments.count >= policy.maxResidentSegments {
evictLeastRecentlyUsed()
}
while currentMemoryBytes + newSegment.estimatedMemoryBytes > policy.memoryBudgetBytes {
evictLeastRecentlyUsed()
}
}
private func evictLeastRecentlyUsed() {
guard !segments.isEmpty else { return }
var oldestTime = CFAbsoluteTimeGetCurrent()
var oldestIndex = 0
for (index, segment) in segments.enumerated() {
let accessTime = lastAccessTime[segment.lowerSpineIndex] ?? 0
if accessTime < oldestTime {
oldestTime = accessTime
oldestIndex = index
}
}
let evicted = segments.remove(at: oldestIndex)
currentMemoryBytes -= evicted.estimatedMemoryBytes
lastAccessTime.removeValue(forKey: evicted.lowerSpineIndex)
}
private func canMerge(_ lhs: RDEPUBBackgroundCoverageSegment, _ rhs: RDEPUBBackgroundCoverageSegment) -> Bool {
guard lhs.renderSignature == rhs.renderSignature else { return false }
let overlap = lhs.upperSpineIndex >= rhs.lowerSpineIndex - 1 &&
rhs.upperSpineIndex >= lhs.lowerSpineIndex - 1
return overlap
}
private func mergeSegments(_ lhs: RDEPUBBackgroundCoverageSegment, _ rhs: RDEPUBBackgroundCoverageSegment) -> RDEPUBBackgroundCoverageSegment? {
let newLower = min(lhs.lowerSpineIndex, rhs.lowerSpineIndex)
let newUpper = max(lhs.upperSpineIndex, rhs.upperSpineIndex)
let newChapterCount = newUpper - newLower + 1
if newChapterCount > policy.maxChaptersPerSegment {
return nil
}
let newResolved = lhs.resolvedSpineIndices.union(rhs.resolvedSpineIndices)
let newerSegment = lhs.generatedAt <= rhs.generatedAt ? rhs : lhs
let olderSegment = lhs.generatedAt <= rhs.generatedAt ? lhs : rhs
let newPageMap = mergePageMaps(olderSegment.pageMap, newerSegment.pageMap)
return RDEPUBBackgroundCoverageSegment(
lowerSpineIndex: newLower,
upperSpineIndex: newUpper,
pageMap: newPageMap,
resolvedSpineIndices: newResolved,
generatedAt: max(lhs.generatedAt, rhs.generatedAt),
renderSignature: lhs.renderSignature,
estimatedMemoryBytes: estimateMemoryBytes(pageMap: newPageMap, resolvedCount: newResolved.count)
)
}
private func mergePageMaps(_ older: RDEPUBBookPageMap, _ newer: RDEPUBBookPageMap) -> RDEPUBBookPageMap {
var builder = RDEPUBBookPageMap.Builder()
var entriesBySpineIndex: [Int: RDEPUBBookPageMapEntry] = [:]
for entry in older.entries {
entriesBySpineIndex[entry.spineIndex] = entry
}
for entry in newer.entries {
entriesBySpineIndex[entry.spineIndex] = entry
}
for spineIndex in entriesBySpineIndex.keys.sorted() {
guard let entry = entriesBySpineIndex[spineIndex] else { continue }
builder.add(
spineIndex: entry.spineIndex,
href: entry.href,
title: entry.title,
pageCount: entry.pageCount,
fragmentOffsets: entry.fragmentOffsets
)
}
return builder.build()
}
private func estimateMemoryBytes(pageMap: RDEPUBBookPageMap, resolvedCount: Int) -> Int {
256 + pageMap.entries.count * 96 + resolvedCount * 16
}
}