debug: auto-scroll jitter instrumentation — tick timing, contentSize, loadMore, markRead
This commit is contained in:
@@ -94,15 +94,19 @@ final class ReaderViewModel {
|
||||
func loadMore() async {
|
||||
guard !isLoadingMore, hasMore else { return }
|
||||
isLoadingMore = true
|
||||
print("[SCROLL-DBG] 📥 loadMore START offset=\(offset)")
|
||||
|
||||
do {
|
||||
let list = try await fetchEntries(offset: offset)
|
||||
let count = list.entries.count
|
||||
entries.append(contentsOf: list.entries)
|
||||
total = list.total
|
||||
offset += list.entries.count
|
||||
offset += count
|
||||
hasMore = offset < list.total
|
||||
print("[SCROLL-DBG] 📥 loadMore END appended=\(count) total=\(entries.count) hasMore=\(hasMore)")
|
||||
} catch {
|
||||
self.error = error.localizedDescription
|
||||
print("[SCROLL-DBG] 📥 loadMore FAILED: \(error)")
|
||||
}
|
||||
isLoadingMore = false
|
||||
}
|
||||
|
||||
@@ -112,6 +112,7 @@ struct EntryListView: View {
|
||||
frame.maxY < 30 else { return }
|
||||
|
||||
markedByScroll.insert(entryId)
|
||||
print("[SCROLL-DBG] 📖 markRead id=\(entryId) entriesCount=\(vm.entries.count)")
|
||||
|
||||
if let idx = vm.entries.firstIndex(where: { $0.id == entryId }) {
|
||||
vm.entries[idx].status = "read"
|
||||
|
||||
@@ -94,25 +94,45 @@ struct ScrollViewDriver: UIViewRepresentable {
|
||||
displayLink = nil
|
||||
}
|
||||
|
||||
private var lastTickTime: CFAbsoluteTime = 0
|
||||
private var lastContentSize: CGFloat = 0
|
||||
private var tickCount = 0
|
||||
|
||||
@objc private func tick(_ link: CADisplayLink) {
|
||||
guard let sv = scrollView else {
|
||||
stopAndNotify()
|
||||
return
|
||||
}
|
||||
|
||||
let now = CFAbsoluteTimeGetCurrent()
|
||||
let maxOffset = sv.contentSize.height - sv.bounds.height + sv.contentInset.bottom
|
||||
guard maxOffset > 0 else { return }
|
||||
|
||||
// 60pt/sec at 1.0x speed, scaled by actual frame duration
|
||||
let delta = CGFloat(speed) * 60.0 * CGFloat(link.targetTimestamp - link.timestamp)
|
||||
let newY = min(sv.contentOffset.y + delta, maxOffset)
|
||||
let frameDuration = link.targetTimestamp - link.timestamp
|
||||
let expectedDelta = CGFloat(speed) * 60.0 * CGFloat(frameDuration)
|
||||
let beforeY = sv.contentOffset.y
|
||||
let newY = min(beforeY + expectedDelta, maxOffset)
|
||||
|
||||
sv.contentOffset.y = newY
|
||||
|
||||
// Notify delegate so tab bar minimize behavior triggers
|
||||
let actualDelta = sv.contentOffset.y - beforeY
|
||||
|
||||
// Detect jitter: contentSize changed, or actual delta differs from expected
|
||||
let contentSizeChanged = sv.contentSize.height != lastContentSize
|
||||
let wallDelta = lastTickTime > 0 ? (now - lastTickTime) * 1000 : 0
|
||||
let isJitter = contentSizeChanged || wallDelta > 25 // >25ms between frames = dropped frame
|
||||
|
||||
tickCount += 1
|
||||
if isJitter || tickCount % 120 == 0 {
|
||||
// Log on jitter or every 2 seconds
|
||||
print("[SCROLL-DBG] \(isJitter ? "⚠️ JITTER" : "✅ ok") wallΔ=\(String(format:"%.1f", wallDelta))ms frameDur=\(String(format:"%.1f", frameDuration*1000))ms expΔ=\(String(format:"%.1f", expectedDelta))pt actΔ=\(String(format:"%.1f", actualDelta))pt y=\(Int(sv.contentOffset.y)) contentH=\(Int(sv.contentSize.height)) csChanged=\(contentSizeChanged) speed=\(String(format:"%.2f", speed))")
|
||||
}
|
||||
|
||||
lastTickTime = now
|
||||
lastContentSize = sv.contentSize.height
|
||||
|
||||
originalDelegate?.scrollViewDidScroll?(sv)
|
||||
|
||||
// Stop at bottom
|
||||
if newY >= maxOffset - 1 {
|
||||
stopAndNotify()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user