Files
hapi/ios/HapiTests/TranscriptGeometryTests.swift
T
weishu 0dcc70e7c7 perf(native): optimize iOS transcript refresh and add frame profiling
Preserve hosted rows while propagating current SwiftUI environments and render captures. Add differential refresh regressions, opt-in real-clock iOS/Android profiling, and reproducible performance results.

Validation: typecheck; CLI/hub/web/shared/relay suites (Web rerun with NODE_OPTIONS=--no-experimental-webstorage for Node 26/jsdom compatibility); existing iOS Debug/Release and Android regression runs.
2026-09-10 16:22:36 +08:00

103 lines
5.1 KiB
Swift

import UIKit
import XCTest
@testable import HapiUI
@MainActor
final class TranscriptGeometryTests: XCTestCase {
func testIndexedQueriesMatchLinearGeometryThroughWindowAndWidthChanges() throws {
let layout = TranscriptLayout()
let collection = UICollectionView(frame: CGRect(x: 0, y: 0, width: 390, height: 844), collectionViewLayout: layout)
var ids = (0..<800).map(String.init)
var heights: [String: CGFloat] = [:]
layout.setItems(ids, width: 390)
layout.prepare()
for index in stride(from: 0, to: ids.count, by: 7) {
let height = CGFloat(20 + index % 19 * 87)
heights[ids[index]] = height
let original = try XCTUnwrap(layout.layoutAttributesForItem(at: IndexPath(item: index, section: 0)))
let preferred = original.copy() as! UICollectionViewLayoutAttributes
preferred.size.height = height
_ = layout.invalidationContext(forPreferredLayoutAttributes: preferred, withOriginalAttributes: original)
}
layout.prepare()
@MainActor func verify(width: CGFloat) {
var y: CGFloat = 10
let expected = ids.map { id -> CGRect in
let frame = CGRect(x: 12, y: y, width: width - 24, height: heights[id] ?? 100)
y = frame.maxY + 10
return frame
}
let actualHeight = layout.collectionViewContentSize.height
XCTAssertEqual(actualHeight, y, accuracy: 0.01)
let rects = stride(from: -500, to: Int(y) + 500, by: 377).map {
CGRect(x: 0, y: $0, width: Int(width), height: 844)
} + [.zero, .null, .infinite, CGRect(x: 1000, y: 0, width: 100, height: 100000)]
for rect in rects {
let indices = expected.indices.filter { expected[$0].intersects(rect) }
let actual = layout.layoutAttributesForElements(in: rect) ?? []
XCTAssertEqual(actual.map(\.indexPath.item), indices)
XCTAssertEqual(actual.map(\.frame), indices.map { expected[$0] })
}
}
verify(width: 390)
ids = (800..<820).map(String.init) + Array(ids.dropLast(20))
layout.setItems(ids, width: 390)
layout.prepare()
verify(width: 390)
// Width changes discard stale measurements but retain correct indices.
heights.removeAll()
layout.setItems(ids, width: 430)
layout.prepare()
verify(width: 430)
layout.setItems([], width: 430)
layout.prepare()
XCTAssertTrue(layout.layoutAttributesForElements(in: .infinite)?.isEmpty == true)
XCTAssertNil(layout.layoutAttributesForItem(at: IndexPath(item: 0, section: 0)))
XCTAssertEqual(collection.collectionViewLayout, layout)
}
#if DEBUG
func testVisibleQueriesVisitOnlyAViewportSizedRange() {
let layout = TranscriptLayout()
layout.setItems((0..<800).map(String.init), width: 390)
layout.prepare()
for step in 0..<200 {
let rect = CGRect(x: 0, y: 1000 + step * 350, width: 390, height: 844)
let visible = layout.layoutAttributesForElements(in: rect) ?? []
XCTAssertFalse(visible.isEmpty)
XCTAssertTrue(visible.allSatisfy { $0.frame.intersects(rect) })
XCTAssertLessThanOrEqual(visible.count, 9)
}
print("Transcript geometry: 800 rows / 200 viewport queries / \(layout.visibleQueryProbeCount) probes")
// log2(800) + visible rows + a small boundary allowance, not 800
// CGRect intersection tests on every frame.
XCTAssertLessThanOrEqual(layout.visibleQueryProbeCount, 200 * 32)
}
func testMeasuringRowsDoesNotAllocateLayoutAttributesForTheWholeWindow() throws {
let layout = TranscriptLayout()
let collection = UICollectionView(frame: CGRect(x: 0, y: 0, width: 390, height: 844), collectionViewLayout: layout)
layout.setItems((0..<800).map(String.init), width: 390)
layout.prepare()
let old = try XCTUnwrap(layout.layoutAttributesForItem(at: IndexPath(item: 300, section: 0)))
let oldFrame = old.frame
let allocations = layout.attributeCreationCount
for index in 0..<20 {
let original = try XCTUnwrap(layout.layoutAttributesForItem(at: IndexPath(item: index, section: 0)))
let preferred = original.copy() as! UICollectionViewLayoutAttributes
preferred.size.height += 20
_ = layout.invalidationContext(forPreferredLayoutAttributes: preferred, withOriginalAttributes: original)
layout.prepare()
}
let updated = try XCTUnwrap(layout.layoutAttributesForItem(at: IndexPath(item: 300, section: 0)))
XCTAssertEqual(updated.frame.minY, oldFrame.minY + 400, accuracy: 0.01)
XCTAssertEqual(old.frame, oldFrame, "UIKit's previously returned attributes must remain immutable")
XCTAssertEqual(collection.collectionViewLayout, layout)
let created = layout.attributeCreationCount - allocations
print("Transcript geometry: 20 self-sizing updates / \(created) attributes created")
XCTAssertLessThanOrEqual(created, 40)
}
#endif
}