import XCTest import SwiftUI @testable import Sybil /// Run on an iPhone simulator, with the real Agent view and deterministic local /// messages. Display-link delivery gaps measure main-run-loop responsiveness, /// not GPU presentation, touch latency, or physical-device frame rate. @MainActor final class AgentPerformanceTests: XCTestCase { func testThinkingScrollResponsiveness() async throws { let room = SybilAgentRoom() var rows: [Message] = [] for index in 1...160 { rows.append(Self.message(index)) } try room.receive(.messages(AgentPage(conversationId: "performance", messages: rows, cursor: "160", beforeCursor: nil, hasMore: false)), replay: true) room.flush() let host = UIHostingController(rootView: SybilAgentView(room: room, topInset: 64) .background(SybilTheme.background).environment(\.colorScheme, .dark)) let scene = try XCTUnwrap(UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene }.first) let window = UIWindow(windowScene: scene) window.rootViewController = host window.makeKeyAndVisible() defer { AgentRenderDiagnostics.enabled = false window.isHidden = true window.rootViewController = nil room.stop() } host.view.frame = window.bounds host.view.layoutIfNeeded() try await Task.sleep(for: .seconds(2)) let scroll = try XCTUnwrap(findScroll(host.view)) XCTAssertGreaterThan(scroll.contentSize.height, 10_000) // Move away from the pinned bottom before the controlled sweeps. scroll.setContentOffset(CGPoint(x: 0, y: max(0, scroll.contentSize.height - scroll.bounds.height - 1200)), animated: false) try await Task.sleep(for: .seconds(1)) AgentRenderDiagnostics.enabled = true var reports: [[String: Any]] = [] for phase in ["idle-scroll", "thinking-stationary", "thinking-scroll"] { if phase == "thinking-stationary" { try room.receive(.activity("performance", busyActivity(step: 0))) room.flush() scroll.setContentOffset(CGPoint(x: 0, y: scroll.contentSize.height - scroll.bounds.height + scroll.adjustedContentInset.bottom), animated: false) try await Task.sleep(for: .seconds(1)) XCTAssertTrue(room.snapshot.isTyping) } AgentRenderDiagnostics.counts = [:] let sampler = AgentFrameSampler(scroll: phase == "thinking-stationary" ? nil : scroll) sampler.start() // Changing queue/worker/tool progress every 500 ms exercises real // activity observation while the typing spinner remains active. No IO. for step in 1...24 { try await Task.sleep(for: .milliseconds(500)) if phase != "idle-scroll" { try room.receive(.activity("performance", busyActivity(step: step))) } } var report = sampler.stop() report["phase"] = phase report["procedure"] = "mixed160-changing-activity-v1" report["operations"] = AgentRenderDiagnostics.counts report["contentHeight"] = scroll.contentSize.height // iPhone 16e / iOS 26.5 / default text size reference geometry from // the unoptimized view. A faster result must not shrink the content. XCTAssertEqual(scroll.contentSize.height, phase == "idle-scroll" ? 39_181.333333 : 39_223.333333, accuracy: 1) reports.append(report) } let data = try JSONSerialization.data(withJSONObject: reports, options: [.sortedKeys]) print("AGENT_PERFORMANCE_JSON " + String(decoding: data, as: UTF8.self)) let attachment = XCTAttachment(data: data, uniformTypeIdentifier: "public.json") attachment.name = "agent-performance"; attachment.lifetime = .keepAlways; add(attachment) } private static func message(_ index: Int) -> Message { let own = index % 3 == 0 let content = own ? "Can you review step \(index)? Keep the scroll position and the **literal input** intact.\n\nInclude the next checks." : "### Investigation \(index)\n\nThe worker checked the local transcript and found a repeatable result. This paragraph contains **emphasis**, `inline code`, and a [reference](https://example.com).\n\n- Preserve the reading position while new messages arrive.\n- Keep attachments, queued messages, and status updates visible.\n- Verify the result on the simulator before publishing.\n\n```swift\nlet item = messages[\(index)]\nrender(item)\n```\n\nThe next step is to compare the measurements and report any remaining limitations." return Message(id: "perf-\(index)", createdAt: Date(timeIntervalSince1970: 1_700_000_000 + Double(index * 60)), role: own ? .user : .assistant, content: content, metadata: .object(["kind": .string("message"), "entryId": .string(String(index))])) } private func busyActivity(step: Int) -> AgentActivity { AgentActivity(state: "thinking", isThinking: true, queued: 1 + step % 3, backgroundWorkers: 1 + step % 2, tools: [.init(id: "local-review", name: "review_transcript", status: "running", output: "Checked \(step * 7) of 200 local entries; preparing batch \(step + 1).")]) } private func findScroll(_ view: UIView) -> UIScrollView? { if let scroll = view as? UIScrollView, scroll.contentSize.height > 10_000 { return scroll } return view.subviews.lazy.compactMap { self.findScroll($0) }.first } } @MainActor private final class AgentFrameSampler: NSObject { private weak var scroll: UIScrollView? private var link: CADisplayLink? private var previous: CFTimeInterval? private var started: CFTimeInterval = 0 private var gaps: [Double] = [] private var budgets: [Double] = [] private var origin: CGFloat = 0 private var scrollDistance: CGFloat = 0 init(scroll: UIScrollView?) { self.scroll = scroll } func start() { origin = scroll?.contentOffset.y ?? 0 started = CACurrentMediaTime() let link = CADisplayLink(target: self, selector: #selector(frame(_:))) link.preferredFrameRateRange = CAFrameRateRange(minimum: 60, maximum: 60, preferred: 60) link.add(to: .main, forMode: .common) self.link = link } @objc private func frame(_ link: CADisplayLink) { let now = CACurrentMediaTime() if let previous { gaps.append((now - previous) * 1000); budgets.append((link.targetTimestamp - link.timestamp) * 1000) } previous = now if let scroll { // Constant-speed triangular sweep over 2400 points, one cycle / 8 s. let phase = (now - started).truncatingRemainder(dividingBy: 8) / 4 let delta = CGFloat(phase <= 1 ? phase : 2 - phase) * 2400 let target = max(-scroll.adjustedContentInset.top, origin - delta) scrollDistance += abs(target - scroll.contentOffset.y) scroll.setContentOffset(CGPoint(x: 0, y: target), animated: false) } } func stop() -> [String: Any] { link?.invalidate(); link = nil let sorted = gaps.sorted() let budget = budgets.sorted()[budgets.count / 2] func percentile(_ value: Double) -> Double { sorted[min(sorted.count - 1, Int(Double(sorted.count - 1) * value))] } return ["seconds": CACurrentMediaTime() - started, "callbacks": gaps.count, "medianBudgetMS": budget, "p50MS": percentile(0.5), "p95MS": percentile(0.95), "p99MS": percentile(0.99), "maxMS": sorted.last ?? 0, "over25MS": gaps.filter { $0 > 25 }.count, "over50MS": gaps.filter { $0 > 50 }.count, "missedBudgetIntervals": gaps.reduce(0) { $0 + max(0, Int(($1 / budget).rounded()) - 1) }, "scrollDistance": scrollDistance] } }