- Rename source module from RDReaderView to RDEpubReaderView - Move all source files from Sources/RDReaderView/ to Sources/RDEpubReaderView/ - Update podspec: RDReaderView.podspec -> RDEpubReaderView.podspec - Update Podfile, demo project, and CocoaPods config for new pod name - Delete old RDReaderView pod support files from ReadViewDemo/Pods - Add new RDEpubReaderView pod support files - Update documentation (API ref, architecture, UML, conventions, etc.) - Add FixedLayoutRotationTests - Update .gitignore: exclude .DS_Store, manual unpack backups, _ssoft-output
179 lines
4.9 KiB
Swift
179 lines
4.9 KiB
Swift
import Foundation
|
|
|
|
struct RDEPUBBackgroundPriorityPolicy {
|
|
|
|
let hotRadius: Int
|
|
|
|
let warmRadius: Int
|
|
|
|
let maxWarmJumpAnchors: Int
|
|
|
|
let coldLaneShare: Double
|
|
|
|
static let `default` = RDEPUBBackgroundPriorityPolicy(
|
|
hotRadius: 24,
|
|
warmRadius: 96,
|
|
maxWarmJumpAnchors: 2,
|
|
coldLaneShare: 0.15
|
|
)
|
|
|
|
static func adaptive(totalBuildableChapters: Int) -> RDEPUBBackgroundPriorityPolicy {
|
|
let hotRadius = min(max(12, Int(sqrt(Double(totalBuildableChapters)))), 48)
|
|
let warmRadius = min(max(hotRadius * 3, 32), 192)
|
|
return RDEPUBBackgroundPriorityPolicy(
|
|
hotRadius: hotRadius,
|
|
warmRadius: warmRadius,
|
|
maxWarmJumpAnchors: 2,
|
|
coldLaneShare: 0.15
|
|
)
|
|
}
|
|
}
|
|
|
|
enum RDEPUBPriorityBand: Int, Comparable {
|
|
case hot = 0
|
|
case warmPrimary = 1
|
|
case warmSecondary = 2
|
|
case cold = 3
|
|
|
|
static func < (lhs: RDEPUBPriorityBand, rhs: RDEPUBPriorityBand) -> Bool {
|
|
lhs.rawValue < rhs.rawValue
|
|
}
|
|
}
|
|
|
|
struct RDEPUBWarmJumpAnchor {
|
|
let spineIndex: Int
|
|
let timestamp: CFAbsoluteTime
|
|
let sequenceNumber: Int
|
|
}
|
|
|
|
struct RDEPUBMetadataParseWorkItem {
|
|
let spineIndex: Int
|
|
let generation: Int
|
|
let priorityBand: RDEPUBPriorityBand
|
|
|
|
var sortKey: (bandRank: Int, distanceToCurrent: Int, distanceToNewestJump: Int, spineIndex: Int) {
|
|
(priorityBand.rawValue, 0, 0, spineIndex)
|
|
}
|
|
}
|
|
|
|
final class RDEPUBBackgroundPriorityManager {
|
|
|
|
private unowned let context: RDEPUBReaderContext
|
|
|
|
private(set) var policy: RDEPUBBackgroundPriorityPolicy
|
|
|
|
private var warmAnchors: [RDEPUBWarmJumpAnchor] = []
|
|
private let warmAnchorsLock = NSLock()
|
|
|
|
private(set) var currentGeneration: Int = 0
|
|
|
|
private var coldCursor: Int = 0
|
|
|
|
init(context: RDEPUBReaderContext) {
|
|
self.context = context
|
|
self.policy = .default
|
|
}
|
|
|
|
func updatePolicy(_ newPolicy: RDEPUBBackgroundPriorityPolicy) {
|
|
policy = newPolicy
|
|
}
|
|
|
|
func addWarmAnchor(spineIndex: Int) {
|
|
let anchor = RDEPUBWarmJumpAnchor(
|
|
spineIndex: spineIndex,
|
|
timestamp: CFAbsoluteTimeGetCurrent(),
|
|
sequenceNumber: currentGeneration
|
|
)
|
|
|
|
warmAnchorsLock.lock()
|
|
warmAnchors.insert(anchor, at: 0)
|
|
|
|
if warmAnchors.count > policy.maxWarmJumpAnchors {
|
|
warmAnchors = Array(warmAnchors.prefix(policy.maxWarmJumpAnchors))
|
|
}
|
|
warmAnchorsLock.unlock()
|
|
|
|
currentGeneration += 1
|
|
coldCursor = 0
|
|
|
|
}
|
|
|
|
func makeMetadataPriorityOrder(
|
|
allBuildableIndices: [Int],
|
|
currentSpineIndex: Int?,
|
|
cachedSpineIndices: Set<Int>
|
|
) -> [Int] {
|
|
let uncachedIndices = allBuildableIndices.filter { !cachedSpineIndices.contains($0) }
|
|
guard !uncachedIndices.isEmpty else { return [] }
|
|
|
|
// M-03: Snapshot warmAnchors under lock since it's written on main thread
|
|
// and read on background thread.
|
|
warmAnchorsLock.lock()
|
|
let warmAnchorsSnapshot = warmAnchors
|
|
warmAnchorsLock.unlock()
|
|
|
|
let items = uncachedIndices.map { spineIndex -> (spineIndex: Int, band: RDEPUBPriorityBand) in
|
|
let band = classifySpineIndex(
|
|
spineIndex: spineIndex,
|
|
currentSpineIndex: currentSpineIndex,
|
|
warmAnchors: warmAnchorsSnapshot
|
|
)
|
|
return (spineIndex, band)
|
|
}
|
|
|
|
let sorted = items.sorted { lhs, rhs in
|
|
|
|
if lhs.band != rhs.band {
|
|
return lhs.band < rhs.band
|
|
}
|
|
|
|
let lhsDistanceToCurrent = currentSpineIndex.map { abs(lhs.spineIndex - $0) } ?? Int.max
|
|
let rhsDistanceToCurrent = currentSpineIndex.map { abs(rhs.spineIndex - $0) } ?? Int.max
|
|
if lhsDistanceToCurrent != rhsDistanceToCurrent {
|
|
return lhsDistanceToCurrent < rhsDistanceToCurrent
|
|
}
|
|
|
|
return lhs.spineIndex < rhs.spineIndex
|
|
}
|
|
|
|
return sorted.map { $0.spineIndex }
|
|
}
|
|
|
|
private func classifySpineIndex(
|
|
spineIndex: Int,
|
|
currentSpineIndex: Int?,
|
|
warmAnchors: [RDEPUBWarmJumpAnchor]
|
|
) -> RDEPUBPriorityBand {
|
|
|
|
if let current = currentSpineIndex {
|
|
let distance = abs(spineIndex - current)
|
|
if distance <= policy.hotRadius {
|
|
return .hot
|
|
}
|
|
}
|
|
|
|
for (index, anchor) in warmAnchors.enumerated() {
|
|
let distance = abs(spineIndex - anchor.spineIndex)
|
|
if distance <= policy.warmRadius {
|
|
return index == 0 ? .warmPrimary : .warmSecondary
|
|
}
|
|
}
|
|
|
|
return .cold
|
|
}
|
|
|
|
func currentWarmAnchors() -> [RDEPUBWarmJumpAnchor] {
|
|
warmAnchorsLock.lock()
|
|
defer { warmAnchorsLock.unlock() }
|
|
return warmAnchors
|
|
}
|
|
|
|
func reset() {
|
|
warmAnchorsLock.lock()
|
|
warmAnchors.removeAll()
|
|
warmAnchorsLock.unlock()
|
|
currentGeneration = 0
|
|
coldCursor = 0
|
|
}
|
|
}
|