ReadViewSDK/ReadViewDemo/ReadViewDemoUITests/ReaderUITests/MemoryFootprintTests.swift
shenlei 83dfa40299 完善搜索定位与内存监测回归验证
本次提交围绕搜索链路的稳定性、定位恢复体验以及长章节内存优化的验证能力进行了补强。

主要改动:

1. 调整阅读器搜索栏、搜索协调器与定位恢复逻辑,改善搜索结果跳转、状态同步与相关 UI 行为。

2. 补充内存探针接入点与上下文记录,便于跟踪阅读过程中的内存占用变化。

3. 更新 RDURLReaderController、ReaderContext 与工具视图相关实现,使调试与观测链路更完整。

4. 新增 MemoryFootprintTests,并同步更新 DemoReaderState 与 SearchTests,用 UI 测试覆盖搜索与内存相关回归场景。
2026-07-09 18:46:04 +09:00

148 lines
5.9 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import XCTest
/// Quantitative memory-footprint harness for the long-chapter optimization
/// (Doc/LONG_CHAPTER_MEMORY_OPTIMIZATION_PLAN.md, P0-2 / P1-4 / P2). Reads
/// phys_footprint back through the demo state `footprintMB` field, so these
/// tests double as a decision tool: the recorded numbers are attached to the
/// result, and the assertions are deliberately loose upper bounds that only
/// catch gross regressions (e.g. each visible page pinning a full-chapter
/// copy), not few-MB noise.
final class MemoryFootprintTests: XCTestCase {
private let app = XCUIApplication()
private let largeBookQuery = "凡人修仙传"
/// Turning 20 pages must not grow footprint beyond this over the post-open
/// baseline. Sized to flag "per-page full-chapter copy" class leaks while
/// tolerating cache warmup and simulator noise.
private let pageTurnGrowthBudgetMB = 180.0
/// A settings change rebuilds display objects; peak may spike transiently
/// but must settle back within this of the pre-change reading.
private let settingsChangeGrowthBudgetMB = 220.0
override func setUpWithError() throws {
continueAfterFailure = false
}
func testFootprintStableAcrossTwentyPageTurns() throws {
app.launchAndOpenSampleBook(
bookTitleQuery: largeBookQuery,
displayType: "scroll",
resetsReaderState: true
)
app.waitForReader(timeout: 20)
app.waitForDemoReaderState(timeout: 8, description: "display=horizontalScroll") {
$0.display == "horizontalScroll"
}
let baseline = try requireFootprint(description: "打开长书后基线")
let paging = app.collectionViews[IDs.readerPaging]
XCTAssertTrue(paging.waitForExistence(timeout: 5), "分页滚动视图未出现")
var readings: [Double] = [baseline]
var lastPage = app.currentDemoReaderState()?.page ?? 0
var turnsMade = 0
for _ in 0..<20 {
paging.swipeLeft()
let state = app.waitForDemoReaderState(timeout: 8, description: "翻页后页码变化") { state in
guard let page = state.page else { return false }
return page != lastPage
}
guard let page = state.page, page != lastPage else { break }
lastPage = page
turnsMade += 1
if let footprint = state.footprintMB {
readings.append(footprint)
}
}
let peak = readings.max() ?? baseline
let final = readings.last ?? baseline
let growth = final - baseline
attachReport(
name: "page-turns",
lines: [
"turnsMade=\(turnsMade)",
"baselineMB=\(fmt(baseline))",
"peakMB=\(fmt(peak))",
"finalMB=\(fmt(final))",
"growthMB=\(fmt(growth))",
"budgetMB=\(fmt(pageTurnGrowthBudgetMB))",
"series=\(readings.map(fmt).joined(separator: ","))"
]
)
XCTAssertGreaterThan(turnsMade, 0, "未能翻动任何页,无法测量翻页内存")
XCTAssertLessThan(
growth, pageTurnGrowthBudgetMB,
"连续翻页后常驻内存增长 \(fmt(growth))MB 超出预算 \(fmt(pageTurnGrowthBudgetMB))MB疑似每页驻留整章副本"
)
}
func testFootprintSettlesAfterFontSizeChange() throws {
app.launchAndOpenSampleBook(
bookTitleQuery: largeBookQuery,
resetsReaderState: true
)
app.waitForReader(timeout: 20)
app.showReaderChromeIfNeeded()
let before = try requireFootprint(description: "改字号前")
XCTAssertTrue(app.buttons[IDs.readerSettings].waitForExistence(timeout: 5), "设置按钮不存在")
app.buttons[IDs.readerSettings].tap()
XCTAssertTrue(app.scrollViews[IDs.settingsScroll].waitForExistence(timeout: 5), "设置面板未出现")
XCTAssertTrue(app.buttons[IDs.settingsFontIncrease].waitForExistence(timeout: 3), "字号增大按钮不存在")
app.buttons[IDs.settingsFontIncrease].tap()
app.buttons[IDs.settingsFontIncrease].tap()
XCTAssertTrue(app.buttons[IDs.settingsDone].waitForExistence(timeout: 3), "完成按钮不存在")
app.buttons[IDs.settingsDone].tap()
// Allow repagination + display-cache rebuild to settle.
RunLoop.current.run(until: Date().addingTimeInterval(2.5))
let after = try requireFootprint(description: "改字号并重排后")
let growth = after - before
attachReport(
name: "font-size-change",
lines: [
"beforeMB=\(fmt(before))",
"afterMB=\(fmt(after))",
"growthMB=\(fmt(growth))",
"budgetMB=\(fmt(settingsChangeGrowthBudgetMB))"
]
)
XCTAssertLessThan(
growth, settingsChangeGrowthBudgetMB,
"改字号重排后常驻内存增长 \(fmt(growth))MB 超出预算 \(fmt(settingsChangeGrowthBudgetMB))MB疑似旧显示对象未释放"
)
}
// MARK: - Helpers
private func requireFootprint(description: String) throws -> Double {
let state = app.waitForDemoReaderState(timeout: 8, description: description) {
($0.footprintMB ?? 0) > 0
}
guard let footprint = state.footprintMB, footprint > 0 else {
throw XCTSkip("demo 状态未暴露 footprintMB跳过内存测量\(state.rawValue)")
}
return footprint
}
private func fmt(_ value: Double) -> String {
String(format: "%.1f", value)
}
private func attachReport(name: String, lines: [String]) {
let text = lines.joined(separator: "\n")
let attachment = XCTAttachment(string: text)
attachment.name = "memory-\(name)"
attachment.lifetime = .keepAlways
add(attachment)
print("[MemoryFootprintTests] \(name)\n\(text)")
}
}